{"id":898,"date":"2020-07-24T11:21:15","date_gmt":"2020-07-24T11:21:15","guid":{"rendered":"http:\/\/spiderwiz.org\/project\/?page_id=898"},"modified":"2025-04-09T06:15:58","modified_gmt":"2025-04-09T06:15:58","slug":"lesson-8","status":"publish","type":"page","link":"https:\/\/spiderwiz.org\/project\/tutorial\/lesson-8\/","title":{"rendered":"Lesson 8: Streaming Queries and Data Object Archiving"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">In <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-7\/\">the previous lesson<\/a> 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 \u2013 in both cases each application replied with a single reply object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 \u201cshow my message history\u201d feature.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <em>Data Object Archiving<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Again we will throw a new microservice into our service mesh. We call it <code>Archiver<\/code> \u2013 an application that listens to all chat messages circulating in the network, archives them and restores them from the archive on user requests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We start with a new class and modifications in the <code>objectLib<\/code> library. For history requests we use <code>ChatHistoryQuery<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.objectLib;\n\nimport org.spiderwiz.annotation.WizField;\nimport org.spiderwiz.core.QueryObject;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * A Query Object used for requesting chat message history. Used as a Streaming Query.\n *\/\npublic class ChatHistoryQuery extends QueryObject{\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n        public final static String ObjectCode = &quot;HSTRQWR&quot;;\n\n        @WizField private String username;          \/\/ the name of the user requesting the history (query field)\n        @WizField private ZDate since;              \/\/ the starting date of the requested history (query field)\n        @WizField private String room;              \/\/ the name of the room the message is sent in (reply field)\n        @WizField private ZDate messageDate;        \/\/ date and time of the message (reply field)\n        @WizField private String message;           \/\/ message content (reply field)\n\n    public String getUsername() {\n        return username;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public ZDate getSince() {\n        return since;\n    }\n\n    public void setSince(ZDate since) {\n        this.since = since;\n    }\n\n    public String getRoom() {\n        return room;\n    }\n\n    public void setRoom(String room) {\n        this.room = room;\n    }\n\n    public ZDate getMessageDate() {\n        return messageDate;\n    }\n\n    public void setMessageDate(ZDate messageDate) {\n        this.messageDate = messageDate;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The query properties are <code>username<\/code> for identifying the inquiring user and <code>since<\/code> for specifying since when history is requested. The other properties \u2013 <code>room<\/code>, <code>messageDate<\/code> and <code>message<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Chatter<\/code> class, known from the previous lessons, has a new boolean <code>stealth<\/code> property. The <code>Archiver<\/code> 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 <code>stealth<\/code> to <code>true<\/code> prevents these notifications as we will see below. Here is the updated <code>Chatter<\/code> class:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.objectLib;\n\nimport org.spiderwiz.annotation.WizField;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * A data object that representing a chat user in a specific chat room\n *\/\npublic class Chatter extends DataObject {\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n    public final static String ObjectCode = &quot;CHTTR&quot;;\n    \n    @WizField private String name;              \/\/ Contains the name of the chatter\n    @WizField private ZDate birthday;           \/\/ Contains the chatter birth date\n    @WizField private boolean stealth = false;  \/\/ True if chatters shall not be notified when this user joins or leave a room\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public ZDate getBirthday() {\n        return birthday;\n    }\n\n    public void setBirthday(ZDate birthday) {\n        this.birthday = birthday;\n    }\n\n    public boolean isStealth() {\n        return stealth;\n    }\n\n    public void setStealth(boolean stealth) {\n        this.stealth = stealth;\n    }\n\n    \/**\n     * @return ChatRoom object code\n     *\/\n    @Override\n    protected String getParentCode() {\n        return ChatRoom.ObjectCode;\n    }\n\n    \/**\n     * @return false because objects of this class are persistent\n     *\/\n    @Override\n    protected boolean isDisposable() {\n        return false;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Back to the Archiver, let\u2019s see <code>ArchiverMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport java.util.List;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.objectLib.ChatHistoryQuery;\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.tutorial.objectLib.ConsoleMain;\n\n\/**\n * Provides the entry point of the application. Initializes and executes the Spiderwiz framework.\n *\/\npublic class ArchiverMain extends ConsoleMain {\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String CONF_FILENAME = &quot;archiver.conf&quot;;\n    private static final String APP_NAME = &quot;Chat Archiver&quot;;\n    private static final String APP_VERSION = &quot;Z1.01&quot;;  \/\/ Version Z1.01: Initial version\n    static final String MY_USERNAME = &quot;_chat_message_archiver&quot;;  \/\/ Use this as the &quot;chatter&quot; name when joining a room\n\n    \/**\n     * Class constructor with constant parameters. Call super constructor and create the user map.\n     *\/\n    public ArchiverMain() {\n        super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION);\n    }\n    \n    \/**\n     * @return the class instance as ArchiverMain type.\n     *\/\n    public static ArchiverMain getInstance() {\n        return (ArchiverMain)ConsoleMain.getInstance();\n    }\n\n    \/**\n     * Application entry point. Instantiate the class and initialize the instance.\n     * \n     * @param args the command line arguments. Not used in this application.\n     *\/\n    public static void main(String[] args) {\n        new ArchiverMain().init();\n    }\n\n    \/**\n     * @return the list of produced objects\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            Chatter.ObjectCode              \/\/ To notify when we join a room (we will join every room)\n        };\n    }\n\n    \/**\n     * @return the list of consumed objects\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{\n            ChatRoom.ObjectCode,            \/\/ Needs to know about every new room\n            Chatter.ObjectCode,             \/\/ Needs to know the name of every chatter\n            ChatMessage.ObjectCode,         \/\/ Listens to every message for archiving.\n            ChatHistoryQuery.ObjectCode     \/\/ Replier of this query.\n        };\n    }\n\n    \/**\n     * Add implementation classes to the object factory list of this application.\n     * @param factoryList\n     *\/\n    @Override\n    protected void populateObjectFactory(List&lt;Class&lt;? extends DataObject&gt;&gt; factoryList) {\n        super.populateObjectFactory(factoryList);\n        factoryList.add(ChatRoomArchiver.class);\n        factoryList.add(Chatter.class);\n        factoryList.add(ChatMessageArchiver.class);\n        factoryList.add(ChatHistoryQueryReply.class);\n        factoryList.add(ArchivedUser.class);\n        factoryList.add(ArchivedChatItem.class);\n    }\n    \n    \/**\n     * Process an input line - nothing to process in our case.\n     * @param line      The input line\n     * @return true\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Archiver<\/code> consumes (line 58) the following object types:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><code>ChatRoom<\/code>: because it needs to join every room.<\/li><li><code>Chatter<\/code>: because it needs to know the name of every chatter.<\/li><li><code>ChatMessage<\/code>: because it needs to archive every message.<\/li><li><code>ChatHistoryQuery<\/code>: this is the query object defined in this lesson that is used by chat applications to request chat history.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Archiver<\/code> produces (line 48) one object type \u2013 <code>Chatter<\/code>, 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The implementation classes of these classes are registered in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#populateObjectFactory(java.util.List)\">populateObjectFactory()<\/a> (line 72). You will notice that we register two more implementation classes that the application neither produces nor consumes \u2013 <code>ArchivedUser<\/code> and <code>ArchivedChatItem<\/code>. These are used internally for the archiving work as you will see in a moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Archiver<\/code> does not do any command line processing, except for exiting on the <code>exit<\/code> command, so <code>processConsoleLine()<\/code>, which is an abstract method that must be implemented, just returns <code>true<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s take a look at <code>ChatRoomArchiver<\/code>, the implementation extension of <code>ChatRoom<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\n\n\/**\n * An implementation class of ChatRoom for the archiver.\n *\/\npublic class ChatRoomArchiver extends ChatRoom {\n\n    \/**\n     * Join every room that is created so that it can archive the entire system.\n     *\/\n    @Override\n    protected void onNew() {\n        try {\n            Chatter chatter = createChild(Chatter.class, ArchiverMain.getInstance().getAppUUID().toString());\n            chatter.setName(ArchiverMain.MY_USERNAME);\n            chatter.setStealth(true);       \/\/ We don't want chatters to see when we join or leave a room.\n            chatter.commit();\n            System.out.printf(&quot;Joined room %sn&quot;, getObjectID());\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Theoretically arriving here if Chatter does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            ArchiverMain.getInstance().sendExceptionMail(ex, &quot;Cannot instantiate Chatter class&quot;, null, false);\n        }\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Recall that we consume <code>ChatRoom<\/code> because we want to join every room. We do it by overriding <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onNew()\">onNew()<\/a>, in which we create a <code>Chatter<\/code> child on every chat room that enters our space. Note that \u201cnew\u201d in the <code>onNew<\/code> context means \u201cnew to this application\u201d. This makes a difference when <em>Room Manager<\/em> is active and creates rooms before <code>Archiver<\/code> is launched, in which case the room is not really new, but it appears on the consuming application\u2019s radar as a new object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The ID of the created <code>Chatter<\/code> object is the current application\u2019s UUID as before. The chatter name is set in our case to <code>\u201c_chat_message_archiver\u201d<\/code>. We have added a new property to the <code>Chatter<\/code> class \u2013 <code>stealth<\/code> that we set here to <code>true<\/code>. We commit the object, print a message and are done.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now to <code>ChatMessageArchiver<\/code>, the implementation extension of <code>ChatMessage<\/code>, where we trap every circulated chat message and archive it:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * Implementation class of ChatMessagee for the archiver.\n *\/\npublic class ChatMessageArchiver extends ChatMessage {\n\n    \/**\n     * Every arriving message is encapsulated in ArchivedChatItem and archived.\n     * @return true\n     *\/\n    @Override\n    protected boolean onEvent() {\n        try {\n            ArchivedChatItem item =\n                ArchiverMain.createTopLevelObject(ArchivedUser.class, ((Chatter)getParent()).getName()).\n                    createChild(ArchivedChatItem.class, null);\n            item.archive(ZDate.now(), getParent().getParent().getObjectID(), getMessage());\n        } catch (Exception ex) {\n            ArchiverMain.getInstance().sendExceptionMail(ex, &quot;When trying to archive a chat item&quot;, null, false);\n        }\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Requests for archive retrievals come from specific users, therefore we need to classify the archive by users. We do it in the overriding <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onEvent()\">onEvent()<\/a> method by creating a top level <code>ArchivedUser<\/code> object for the given user and then creating a child <code>ArchivedChatItem<\/code> object under it. We use <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#createTopLevelObject(java.lang.Class,java.lang.String)\">createTopLevelObject()<\/a> 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 <code>ArchivedChatItem.archive()<\/code> method that we will see in a moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As mentioned above, <code>ArchivedUser<\/code> and <code>ArchivedChatItem<\/code> are neither produced nor consumed but just used internally in this application, therefore they are placed in the project\u2019s package&nbsp; rather than in <code>objectLib<\/code>. Here they are:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport org.spiderwiz.core.DataObject;\n\n\/**\n * Represents a user whose chat items need to be archived. By itself it has no properties but it serves as the parent of\n * ArchivedChatItem.\n *\/\npublic class ArchivedUser extends DataObject {\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n        public final static String ObjectCode = &quot;HSTRUSR&quot;;\n\n    \/**\n     * @return null since this is a root object\n     *\/\n    @Override\n    protected String getParentCode() {\n        return null;\n    }\n\n    \/**\n     * @return false since this is a persistent object.\n     *\/\n    @Override\n    protected boolean isDisposable() {\n        return false;\n    }\n\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#isDisposable()\">isDisposable()<\/a> returns <code>false<\/code> as you should expect.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport org.spiderwiz.annotation.WizField;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * Hold one chat history item for archiving and restoration\n *\/\npublic class ArchivedChatItem extends DataObject {\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n    public final static String ObjectCode = &quot;HSTRITM&quot;;\n\n    @WizField private ZDate messageDate;\n    @WizField private String room;\n    @WizField private String message;\n\n    public ZDate getMessageDate() {\n        return messageDate;\n    }\n\n    public void setMessageDate(ZDate messageDate) {\n        this.messageDate = messageDate;\n    }\n\n    public String getRoom() {\n        return room;\n    }\n\n    public void setRoom(String room) {\n        this.room = room;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n    \n    \/**\n     * Each restored item is posted as a replyNext() call on the ChatHistoryQuery query.\n     * @param associated    the object associated with the restoration, in this case the ChatHistoryQuery that activated it.\n     * @return true\n     *\/\n    @Override\n    protected boolean onRestore(Object associated) {\n        ((ChatHistoryQueryReply)associated).replyOneItem(getMessageDate(), getRoom(), getMessage());\n        return true;\n    }\n\n    \/**\n     * @return the Object Code of ArchivedUser.\n     *\/\n    @Override\n    protected String getParentCode() {\n        return ArchivedUser.ObjectCode;\n    }\n\n    \/**\n     * @return true since chat items are disposable.\n     *\/\n    @Override\n    protected boolean isDisposable() {\n        return true;\n    }\n\n    \/**\n     * Enable archiving by defining the archive path\n     * @return archive path constructed by &quot;chat\/&lt;user name&gt;\/&lt;date&gt;\/&lt;hour&gt;.arc&quot;\n     *\/\n    @Override\n    protected String getArchivePath() {\n        return &quot;chat\/#1\/#y#m#d\/#h&quot;;\n    }\n    \n    \/**\n     * Set values in the object and archive it\n     * @param messageDate\n     * @param room\n     * @param message\n     * @throws Exception \n     *\/\n    void archive(ZDate messageDate, String room, String message) throws Exception {\n        setMessageDate(messageDate);\n        setRoom(room);\n        setMessage(message);\n        archive();\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">This is the class that we archive. It is a child of <code>ArchivedUser<\/code> (line 59), it is disposable (line 67) and it has three properties \u2013 <code>messageDate<\/code>, <code>room<\/code> and <code>message<\/code>. It has an internal method &#8211; <code>archive(ZDate messageDate, String room, String message)<\/code> (line 87) \u2013 that is called from <code>ChatMessageArchiver.onEvent()<\/code> as we saw above. The method stores the given values in the object\u2019s properties and calls <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#archive()\">archive()<\/a> to archive the object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The most interesting method that <code>ArchivedChatItem<\/code> overrides is <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#getArchivePath()\">getArchivePath()<\/a> (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 <code>\"chat\/#1\/#y#m#d\/#h\"<\/code>, which means the following structure:<\/p>\n\n\n\n<div class=\"spiderwiz-folder\">\n   <ul style=\"list-style-image:url('http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/openFolderSmall.png')\">\n      <li>\n         <code>archive<\/code> (Archive root folder name, see below)\n         <ul>\n            <li>\n               <code>chat<\/code> (the sub-folder for <code>ArchivedChatItem<\/code> archive)\n               <ul>\n                  <li>\n                     <code><code>user-name<\/code><\/code> (<code>#1<\/code> means key of one level above the archived item, which is the key of <code>ArchivedUser<\/code>, i.e. the user name)\n                     <ul>\n                        <li>\n                           <code><code>yymmdd<\/code><\/code> (date pattern as indicated by <code>#y#m#d<\/code>)\n                           <ul style=\"list-style-image:url('http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/FileArchive.png')\">\n                              <li><code>00.arc<\/code> (file name for 12 am, if exists)<\/li>\n                              <li><code>01.arc<\/code> (file name 1 am, if exists)<\/li>\n                              <li>\u2026<\/li>\n                              <li><code>23.arc<\/code> (file name for 11 pm, if exists)<\/li>\n                           <\/ul>\n                        <\/li>\n                     <\/ul>\n                  <\/li>\n               <\/ul>\n            <\/li>\n         <\/ul>\n      <\/li>\n   <\/ul>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The other overriding method is <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRestore()\">onRestore()<\/a>. 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 <code>ChatHistoryQuery<\/code> that carries the history request. We will see in a second how it works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We are ready to wire it all up with <code>ChatHistoryQueryReply<\/code>, the implementation class of <code>ChatHistoryQuery<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.archiver;\n\nimport org.spiderwiz.tutorial.objectLib.ChatHistoryQuery;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * Implementation class for the replier side of ChatHistoryQuery.\n *\/\npublic class ChatHistoryQueryReply extends ChatHistoryQuery {\n    \n    \/**\n     * Reply to the query by activating restoration of the relevant ArchivedChatItem objects\n     * @return true\n     *\/\n    @Override\n    protected boolean onInquire() {\n        try {\n            restore(ArchivedChatItem.ObjectCode, this, getSince(), null, getUsername(), null);\n            replyEnd();\n        } catch (Exception ex) {\n            ArchiverMain.getInstance().sendExceptionMail(ex, &quot;When replying the query&quot;, null, false);\n        }\n        return false;       \/\/ because the query has already been replied in full.\n    }\n\n    \/**\n     * Override the default streaming rate to specify continuous streaming with no delay.\n     * @return 0\n     *\/\n    @Override\n    protected int getStreamingRate() {\n        return 0;\n    }\n\n    \/**\n     * Reply the streaming query with one item.\n     * @param messageDate\n     * @param room\n     * @param message \n     *\/\n    void replyOneItem(ZDate messageDate, String room, String message) {\n        try {\n            setMessageDate(messageDate);\n            setRoom(room);\n            setMessage(message);\n            replyNext();\n        } catch (Exception ex) {\n            ArchiverMain.getInstance().sendExceptionMail(ex, &quot;When replying a query with one chat item&quot;, null, false);\n        }\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#onInquire()\">onInquire()<\/a> as in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-7\/\">Lesson 7<\/a> (line 16), but instead of filling in reply properties and returning <code>true<\/code>, we start a procedure that generates a stream of objects. Recall that we need to stream chat message history that belongs to <code>username<\/code> starting from the time specified by <code>since<\/code> (or the full history if <code>since<\/code> is <code>null<\/code>). This is done by calling the static <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#restore(java.lang.String,org.spiderwiz.zutils.ZDate,org.spiderwiz.zutils.ZDate,java.lang.String...)\">restore()<\/a> method (line 18) and associating the current object with the call. The object pops up with each activation of <code>ArchivedChatItem.onRestore()<\/code> as we saw above, which calls <code>replyOneItem()<\/code> that we see here (line 42). The method sets the object\u2019s response properties for one item and calls <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#replyNext()\">replyNext()<\/a>. This is repeated for each history item included in the query response.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Back to <code>onInquire()<\/code>, when all retrieved items are streamed back to the inquirer and the <code>restore()<\/code> method finishes, it calls <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#replyEnd()\">replyEnd()<\/a> to mark the end of the stream. All done from this side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before concluding with the <code>Archiver<\/code> note the override of <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#getStreamingRate()\">getStreamingRate()<\/a> (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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It remains to see the <code>Archiver<\/code> configuration file:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/Archiver\/Logs\n[archive folder]\/tests\/Archiver\/Archive\n[producer-1]ip=localhost;port=10001<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">There is one property beyond the usual stuff &#8211; <code>archive folder<\/code>, which defines the root folders for all archives.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s 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:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><code>!history<\/code> \u2013 request the full history of chat messages sent by this user.<\/li><li><code>!history &lt;<em>number of hours<\/em>>h<\/code> \u2013 request history since &lt;<em>number of hours<\/em>> ago, e.g. <code>!history 3h<\/code>.<\/li><li><code>!history &lt;<em>number of days<\/em>>d<\/code> \u2013 request history since &lt;<em>number of days<\/em>> ago, e.g. <code>!history 1d<\/code>.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Here is <code>ChatMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.chat;\n\nimport java.io.PrintStream;\nimport java.text.ParseException;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.objectLib.ActiveUserQuery;\nimport org.spiderwiz.tutorial.objectLib.Chat;\nimport org.spiderwiz.tutorial.objectLib.ChatHistoryQuery;\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.tutorial.objectLib.ConsoleMain;\nimport org.spiderwiz.tutorial.objectLib.LoginQuery;\nimport org.spiderwiz.zutils.ZDate;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Provides the entry point of the application. Initializes and executes the Spiderwiz framework.\n *\/\npublic class ChatMain extends ConsoleMain{\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String APP_NAME = &quot;Chat Room Client&quot;;\n    private static final String APP_VERSION = &quot;Z8.01&quot;;  \/\/ Version Z8.01: Lesson 8 modifications\n\n    private static final String JOIN = &quot;!join&quot;;         \/\/ join a chat room command\n    private static final String LEAVE = &quot;!leave&quot;;       \/\/ leave current chat room command\n    private static final String LOGIN = &quot;!login&quot;;       \/\/ start login procedure\n    private static final String LOGOUT = &quot;!logout&quot;;     \/\/ log the user out\n    private static final String REGISTER = &quot;!register&quot;; \/\/ start registration procedure\n    private static final String HISTORY = &quot;!history&quot;;   \/\/ print my chat history\n    \n    private String myName = null;               \/\/ The user login name\n    private ZDate birthday = null;              \/\/ User birth date. Get it when logging in\n    private Chatter chatter = null;             \/\/ A Chatter object representing the user chatting in a specific room\n    private ChatMessage message = null;         \/\/ A ChatMessage object for committing chat messages\n    private Chat privateMessage = null;         \/\/ A Chat object for committing private messages\n\n    \/**\n     * Class constructor with constant parameters.\n     * @param confFileName  configuration file name, provided as a command argument\n     *\/\n    public ChatMain(String confFileName) {\n        super(ROOT_DIRECTORY, confFileName, APP_NAME, APP_VERSION);\n    }\n    \n    \/**\n     * @return the class instance as ChatMain type.\n     *\/\n    public static ChatMain getInstance() {\n        return (ChatMain)ConsoleMain.getInstance();\n    }\n\n    \/**\n     * Application entry point. Instantiate the class and initialize the instance.\n     * \n     * @param args the command line arguments. The first argument is used as the configuration file name.\n     *\/\n    \/**\n     * @param args the command line arguments\n     *\/\n    public static void main(String[] args) {\n        \/\/ Don't do anything if there is no configuration file name\n        if (args.length == 0) {\n            System.out.println(&quot;Configuration file has not been defined&quot;);\n            return;\n        }\n        new ChatMain(args[0]).init();\n    }\n\n    \/**\n     * Print user instructions to the console.\n     * @return true\n     *\/\n    @Override\n    protected boolean preStart() {\n        System.out.printf(\n            &quot;Welcome to the dynamic chat system.n&quot;\n                + &quot;To log in type !login.n&quot;\n                + &quot;To log out type !logout.n&quot;\n                + &quot;To register type !register.n&quot;\n                + &quot;To join a chat room or change your current chat room, type &quot;!join &lt;room name&gt;&quot;.n&quot;\n                + &quot;To leave your chat room, type &quot;!leave&quot;.n&quot;\n                + &quot;To send a message in the current chat room just type the message &quot;\n                + &quot;(a message cannot start with either '&gt;' or '!').n&quot;\n                + &quot;To send a private message type '&gt;' followed by name of the user you want to send &quot;\n                + &quot;the private message to, followed by ':' for by the message.n&quot;\n                + &quot;To print the history of your own messages, type &quot;!history &lt;hours&gt;h&quot; or &quot;!history &lt;days&gt;d&quot;n&quot;\n                + &quot;(for instance &quot;!history 3h&quot; to see messages since 3 hours ago and &quot;!history 1d&quot; to see messages since 1 day&quot;\n                + &quot; ago).n&quot;\n                + &quot;or type just &quot;!history&quot; to see all your messages.n&quot;\n                + &quot;To exit type 'exit'.n&quot;,\n        );\n        return true;\n    }\n\n  \n    \/**\n     * @return the list of produced objects - Chatter, Chat, ChatMessage and LoginQuery\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            Chatter.ObjectCode,\n            Chat.ObjectCode,\n            ChatMessage.ObjectCode,\n            LoginQuery.ObjectCode,\n            ChatHistoryQuery.ObjectCode\n        };\n    }\n\n    \/**\n     * @return the list of consumed objects - ChatRoom, Chatter, Chat, ChatMessage and ActiveUserQuery\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{\n            ChatRoom.ObjectCode,\n            Chatter.ObjectCode,\n            Chat.ObjectCode,\n            ChatMessage.ObjectCode,\n            ActiveUserQuery.ObjectCode\n        };\n    }\n\n    \/**\n     * Add implementation classes to the object factory list of this application.\n     * @param factoryList\n     *\/\n    @Override\n    protected void populateObjectFactory(List&lt;Class&lt;? extends DataObject&gt;&gt; factoryList) {\n        super.populateObjectFactory(factoryList);\n        factoryList.add(ChatRoomConsume.class);\n        factoryList.add(ChatterConsume.class);\n        factoryList.add(ChatConsume.class);\n        factoryList.add(ChatMessageImp.class);\n        factoryList.add(LoginQuery.class);\n        factoryList.add(ActiveUserQueryReply.class);\n        factoryList.add(ChatHistoryQueryInquire.class);\n    }\n\n    \n    \/**\n     * Process an input line\n     * @param line      the line to be broadcast as a chat message.\n     * @return true if processed successfully\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        try {\n            \/\/ If the line starts with '!' this is a command line\n            if (line.startsWith(&quot;!&quot;)) {\n                String command[] = line.trim().split(&quot;s+&quot;, 2);\n                switch (command[0].toLowerCase()) {\n                case JOIN:\n                    \/\/ if room name is provided join the room\n                    if (command.length &gt; 1)\n                        joinRoom(command[1]);\n                    break;\n                case LEAVE:\n                    \/\/ If the user is in a room, leave it\n                    leaveRoom(null);\n                    break;\n                case LOGIN:\n                    login();\n                    break;\n                case LOGOUT:\n                    logout();\n                    break;\n                case REGISTER:\n                    register();\n                    break;\n                case HISTORY:\n                    if (myName == null)\n                        System.out.println(&quot;You must log in before requesting history.&quot;);\n                    else if (!getHistory(command.length &lt; 2 ? null : command[1]))\n                        System.out.println(&quot;Invalid command argument&quot;);\n                    break;\n                }\n                return true;\n            }\n            \n            \/\/ If the line starts with '&gt;' split the line on the colon (':') and send the test\n            \/\/ after the colon as a private message to the user with the name before the colon.\n            if (line.startsWith(&quot;&gt;&quot;)) {\n                String cmd[] = line.substring(1).split(&quot;:&quot;);\n                if (cmd.length &lt; 2)\n                    return true;\n                String destination = findUser(cmd[0]);\n                if (destination == null)\n                    return true;\n                \/\/ If didn't do yet, create an orphan Chatter object for sending private messages,\n                \/\/ then prepare a ChatMessage object for sending private messages\n                if (privateMessage == null) {\n                    privateMessage = createTopLevelObject(Chat.class, null);\n                    privateMessage.setName(myName);\n                }\n                \/\/ Set message and commit to destination\n                privateMessage.setMessage(cmd[1]);\n                privateMessage.commit(destination);\n                return true;\n            }\n            \n            \/\/ If this is a normal message, make sure the user is in a room and broadcast the message\n            if (message != null) {\n                message.setMessage(line);\n                message.commit();\n            }\n            return true;\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Theoretically arriving here if a data object does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            sendExceptionMail(ex, &quot;Cannot instantiate a data object class&quot;, null, false);\n            return false;\n        }\n    }\n    public String getMyName() {\n        return myName;\n    }\n\n    public ZDate getBirthday() {\n        return birthday;\n    }\n    \n    \/**\n     * Get user name and password and log in.\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     * @throws NoSuchFieldException     If LoginQuery does not define ObjectCode static field.\n     * @throws IllegalAccessException   If LoginQuery.ObjectCode is not public.\n     *\/\n    private PrintStream login() throws NoSuchFieldException, IllegalAccessException {\n        String username;\n        String password;\n\n        \/\/ Check if already logged in\n        if (myName != null)\n            return System.out.printf(&quot;You are already logged in as %s. To log in with another name log out first.n&quot;, myName);\n        \n        \/\/ Create a login query\n        LoginQuery query = createQuery(LoginQuery.class);\n        while(true) {\n            \/\/ Get user name\n            System.out.printf(&quot;(type Enter to exit login)n&quot;);\n            username = readConsoleLine(&quot;User name:&quot;);\n            if (username.isBlank())\n                return exitLogin();\n            do {\n                \/\/ Get password\n                password = readConsoleLine(&quot;Password:&quot;);\n                if (password.isBlank())\n                    return exitLogin();\n\n                \/\/ Post the login query\n                query.setNewUser(false);\n                query.setName(username);\n                query.setPassword(password);\n                query.post(5 * ZDate.SECOND);\n                \n                \/\/ If the query did not expire check login results, if it did, repeat.\n                if (query.waitForReply()) {\n                    switch (query.getResult()) {\n                    case OK:\n                        myName = query.getName();\n                        birthday = query.getBirthday();\n                        return System.out.printf(&quot;Logged in successfully as %s.n&quot;, myName);\n                    case NOT_EXISTS:\n                        System.out.printf(&quot;User %s does not exist. Please try again.n&quot;, username);\n                        break;\n                    case WRONG_PASSWORD:\n                        System.out.printf(&quot;Password did not match, please try again.n&quot;);\n                        break;\n                    }\n                } else\n                    return unavailableUserManager();\n            } while (query.getResult() == LoginQuery.ResultCode.WRONG_PASSWORD);\n        }\n    }\n    \n    \/**\n     * Log out if already logged in\n     *\/\n    private void logout() {\n        if (myName == null) {\n            System.out.printf(&quot;No user is logged in.n&quot;);\n            return;\n        }\n        leaveRoom(null);\n        System.out.printf(&quot;%s is logged out.n&quot;, myName);\n        myName = null;\n        birthday = null;\n    }\n\n    private PrintStream register() throws NoSuchFieldException, IllegalAccessException {\n        String username;\n        String password, password2;\n\n        \/\/ Check if already logged in\n        if (myName != null)\n            return System.out.printf(&quot;You are already logged in as %s. To register a new user log out first.n&quot;, myName);\n        \n        \/\/ Create a registration query\n        LoginQuery query = createQuery(LoginQuery.class);\n        while(true) {\n            \/\/ Get user name\n            System.out.printf(&quot;(type Enter to exit login)n&quot;);\n            username = readConsoleLine(&quot;User name:&quot;);\n            if (username.isBlank())\n                return exitRegistration();\n            do {\n                \/\/ Get password\n                password = readConsoleLine(&quot;Password:&quot;);\n                if (password.isBlank())\n                    return exitRegistration();\n\n                \/\/ Repeat the password\n                password2 = readConsoleLine(&quot;Repeat password:&quot;);\n                if (password2.isBlank())\n                    return exitRegistration();\n                \n                \/\/ Check if passwords are equal\n                if (password.equals(password2))\n                    break;\n                System.out.println(&quot;Passwords do not match.&quot;);\n            } while (true);\n            \n            \/\/ Get and parse birth date\n            do {\n                String s = readConsoleLine(&quot;Date of birth (yyyy\/mm\/dd):&quot;);\n                if (s.isBlank())\n                    return exitRegistration();\n                try {\n                    birthday = ZDate.parseTime(s, &quot;yyyy\/MM\/dd&quot;, null);\n                } catch (ParseException ex) {\n                    birthday = null;\n                    System.out.printf(&quot;Invalid date %s. Please try again.n&quot;, s);\n                }\n            } while (birthday == null);\n\n            \/\/ Post the registration query\n            query.setNewUser(true);\n            query.setName(username);\n            query.setPassword(password);\n            query.setBirthday(birthday);\n            query.post(5 * ZDate.SECOND);\n\n            \/\/ If the query did not expire check login results, if it did, repeat.\n            if (query.waitForReply()) {\n                switch (query.getResult()) {\n                case OK:\n                    myName = username;\n                    return System.out.printf(&quot;Registered and logged in successfully as %s.n&quot;, myName);\n                case EXISTS:\n                    System.out.printf(&quot;User %s already exista. Please try again with another name.n&quot;, username);\n                    break;\n                }\n            } else\n                return unavailableUserManager();\n        }\n    }\n    \n    \/**\n     * Print a message when login existed by typing a blank Enter\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream exitLogin() {\n        return System.out.printf(&quot;Login exitedn&quot;);\n    }\n    \n    \/**\n     * Print a message when registration existed by typing a blank Enter\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream exitRegistration() {\n        return System.out.printf(&quot;Registration exitedn&quot;);\n    }\n    \n    \/**\n     * Print a message when user management is not available\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream unavailableUserManager() {\n        return System.out.printf(&quot;No user management entity is available to handle this requestn&quot;);\n    }\n    \n    \/**\n     * Join a room after checking that the room exists, its adult setting matches user configuration,\n     * and the user is not already there. If the user is in another room leave that room first.\n     * @param room  the new room name\n     * @throws NoSuchFieldException     If any of the created data objects does not define ObjectCode static field.\n     * @throws IllegalAccessException   If ObjectCode is not public.\n     *\/\n    private synchronized void joinRoom(String room) throws NoSuchFieldException, IllegalAccessException {\n        \/\/ User must be logged in\n        if (myName == null) {\n            System.out.printf(&quot;You must log in before joining a room.n&quot;);\n            return;\n        }\n        \n        \/\/ Check whether room exists and the user is not already in the room\n        ChatRoomConsume chatRoom = getRootObject().getChild(ChatRoomConsume.class, room);\n        if (chatRoom == null) {\n            System.out.printf(&quot;Room %s does not existn&quot;, room);\n            return;\n        }\n\n        if (chatter != null &amp;&amp; room.equalsIgnoreCase(chatter.getParent().getObjectID()))\n            return;\n        \n        \/\/ Check if a young chatters is trying to join an adult room\n        if (!chatRoom.canJoin(birthday)) {\n            System.out.printf(&quot;You cannot join room %s because it is only for adultsn&quot;, room);\n            return;\n        }\n        \n        \/\/ Leave current room if any\n        leaveRoom(null);\n        \n        \/\/ Create a Chatter object for the user that joins the specified room\n        chatter = chatRoom.createChild(Chatter.class, getAppUUID().toString());\n        chatter.setName(myName);\n        chatter.setBirthday(birthday);\n        chatter.commit();       \/\/ let everybody know I joined the room\n        \/\/ Prepare a ChatMessage object for sending messages from now on\n        message = chatter.createChild(ChatMessage.class, null);\n        System.out.printf(&quot;Joined room %sn&quot;, room);\n    }\n    \n    \/**\n     * If the user is in the given room, leave it.\n     * @param room  the name of the room to be checked if the user is there. If null, leave the current room without checking\n     *\/\n    public synchronized void leaveRoom(String room) {\n        if (isSameRoom(room)) {\n            DataObject removed = chatter.remove();  \/\/ leave the room\n            if (removed != null)                 \/\/ can be null if the 'chatter' object has already been removed.\n                removed.commit();                \/\/ let everybody know I did\n            System.out.printf(&quot;Left room %sn&quot;, chatter.getParent().getObjectID());\n            chatter = null;\n            message = null;\n        }\n    }\n    \n    \/**\n     * Check if the user is in the same room as the parameter\n     * @param room  the name of the room to be checked if the user is there. If null, return true\n     * @return true if the user in 'room' or 'room' is null\n     *\/\n    public synchronized boolean isSameRoom(String room) {\n        return chatter != null &amp;&amp;\n            (room == null || room.equalsIgnoreCase(chatter.getParent().getObjectID()));\n    }\n    \n    \/**\n     * Check whether the user running the application whose UUID is given is on the same room as us. This is done by getting\n     * our current ChatRoom object and see if it has a child with that ID.\n     * @param appUUID   Application UUID to check\n     * @return  true if the application is in the same room as us.\n     *\/\n    public synchronized boolean isMemberOfMyRoom(UUID appUUID) {\n        try {\n            if (chatter == null)\n                return false;\n            ChatRoom chatRoom = (ChatRoom)chatter.getParent();\n            return chatRoom != null &amp;&amp; chatRoom.getChild(Chatter.class, appUUID.toString()) != null;\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Theoretically arriving here if a data object does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            sendExceptionMail(ex, &quot;Cannot instantiate a data object class&quot;, null, false);\n            return false;\n        }\n    }\n    \n    \/**\n     * Get current room name, if any\n     * @return if the user is currently in a room return the room name, otherwise return null.\n     *\/\n    public synchronized String getCurrentRoom() {\n        return chatter == null ? null : chatter.getParent().getObjectID();\n    }\n    \n    \/**\n     * Find the user whose name equals the parameter\n     * @param name  user name to look for\n     * @return      If a Chatter object for this user found then return its object ID, which is the UUID of the application\n     *              that the given user runs, otherwise return null.\n     * @throws NoSuchFieldException     If any of the referred data objects does not define ObjectCode static field.\n     * @throws IllegalAccessException   If ObjectCode is not public.\n     *\/\n    private String findUser(String name) throws NoSuchFieldException, IllegalAccessException {\n        FindUserFilter filter = new FindUserFilter(name);\n        Collection&lt;Chatter&gt; users = getRootObject().getFilteredChildren(filter);\n        \/\/ If the returned collection is not empty return the object ID (which is a UUID) of the first,\n        \/\/ otherwise return null.\n        for (Chatter user : users) {\n            return user.getObjectID();\n        }\n        return null;\n    }\n    \n    \/**\n     * Execute !history command\n     * @param arg   command argument - &lt;n&gt;H or &lt;n&gt;D\n     * @return  true if command has been parsed successfully.\n     * @throws NoSuchFieldException     If ChatHistoryQuery does not define ObjectCode static field.\n     * @throws IllegalAccessException   If ChatHistoryQuery.ObjectCode is not public.\n     *\/\n    private boolean getHistory(String arg) throws NoSuchFieldException, IllegalAccessException {\n        \/\/ Parse the argument and calculate time\n        ZDate since = null;\n        if (arg != null) {\n            arg = arg.trim().toLowerCase();\n            int n = ZUtilities.parseInt(arg.substring(0, arg.length() - 1));\n            if (n &lt;= 0)\n                return false;\n            int unit;\n            switch (arg.substring(arg.length() - 1)) {\n            case &quot;h&quot;:\n                unit = ZDate.HOUR;\n                break;\n            case &quot;d&quot;:\n                unit = ZDate.DAY;\n                break;\n            default:\n                return false;\n            }\n            since = ZDate.now().add(-n * unit);\n        }\n        \n        \/\/ create a query and post it\n        ChatHistoryQuery query = createQuery(ChatHistoryQuery.class);\n        query.setUsername(myName);\n        query.setSince(since);\n        query.post(5 * ZDate.SECOND);\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The description of the new command has been added to the instructions printed in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#preStart()\">preStart()<\/a> (line 78).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As mentioned above, chat history is requested through the <code>ChatHistoryQuery<\/code> query object. This is added to the produced objects (line 110). Its implementation class for the inquirer side is <code>ChatHistoryQueryInquire<\/code>, registered in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#populateObjectFactory(java.util.List)\">populateObjectFactory()<\/a> (line 142).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The new command is trapped in <code>processConsoleLine()<\/code> (line 175) and is passed to <code>getHistory()<\/code> (line 509) for parsing and execution. To execute the query, we create a <code>ChatHistoryQuery<\/code> object (line 532), set its properties and call its <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#post(long)\">post()<\/a> method with 5-second expiration time ( like a single-response query, except that in this case the time is set per reply \ufe58 see below).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We will see now how the response, in this case a response stream, is handled by <code>ChatHistoryQueryInquire<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.chat;\n\nimport org.spiderwiz.tutorial.objectLib.ChatHistoryQuery;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * Implementing ChatHistoryQuery at the inquirer side.\n *\/\npublic class ChatHistoryQueryInquire extends ChatHistoryQuery {\n    private int messageCount = 0;\n    \n    \/**\n     * Print out the information contained in every reply record and increase message count\n     *\/\n    @Override\n    protected void onReplyNext() {\n        System.out.printf(&quot;%1$s  room:%2$s  message:%3$sn&quot;, getMessageDate().format(ZDate.FULL_DATE), getRoom(), getMessage());\n        ++messageCount;\n    }\n\n    @Override\n    protected void onReplyEnd() {\n        System.out.printf(\n            &quot;You have typed &quot; + (messageCount &gt; 0 ? &quot;%1$d&quot; : &quot;no&quot;) + &quot; messages&quot; +\n                (getSince() == null ? &quot;&quot; : &quot; since %2$s&quot;) +&quot;n&quot;, messageCount,\n                getSince() == null ? null : getSince().format(ZDate.FULL_DATE));\n    }\n\n    @Override\n    protected void onExpire() {\n        System.out.println(&quot;Chat Archiver is not available&quot;);\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Absolutely straightforward. Instead of overriding <code>onReply()<\/code>, the inquirer of a streaming query overrides <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#onReplyNext()\">onReplyNext()<\/a> and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#onReplyEnd()\">onReplyEnd()<\/a>. In the first we print each history item as it arrives, in the second we print a summary when the stream ends.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#onExpire()\">onExpire()<\/a> is activated when the chatting application cannot communicate with the <code>Archiver<\/code>. 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 <code>post()<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is a small change in <code>ChatterConsume<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.chat;\n\nimport org.spiderwiz.tutorial.objectLib.Chatter;\n\n\/**\n * Consumer implementation of Chatter data object. Notify when a chatter enters or leaves my room.\n *\/\npublic class ChatterConsume extends Chatter {\n\n    \/**\n     * Notify when a chatter enters my room.\n     *\/\n    @Override\n    protected void onNew() {\n        \/\/ The room is the parent of this object\n        if (!isStealth() &amp;&amp; ChatMain.getInstance().isSameRoom((getParent().getObjectID())))\n            System.out.printf(&quot;%s entered the roomn&quot;, getName());\n    }\n\n    \/**\n     * Notify when a chatter leaves my room. If the chatter is myself leave the room gracefully.\n     * @return true to confirm the removal.\n     *\/\n    @Override\n    protected boolean onRemoval() {\n        if (ChatMain.getInstance().getAppUUID().toString().equals(getObjectID()))\n            ChatMain.getInstance().leaveRoom(getParent().getObjectID());\n        else if (!isStealth() &amp;&amp; ChatMain.getInstance().isSameRoom(getParent().getObjectID()))\n            System.out.printf(&quot;%s left the roomn&quot;, getName());\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">You apparently remember that we added a <code>stealth<\/code> property to the <code>Chatter<\/code> class. Here we check it in both <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onNew()\">onNew()<\/a> (line 14) and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRemoval()\">onRemoval()<\/a> (line 25) to ensure that the <code>Archiver<\/code> does not manifest itself when it joins or leaves a chat room.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <em>Room Manager<\/em> and <em>User Manager<\/em>, and see how much you can do with so little code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-7\/\">previous lesson<\/a> we built <em>User Manager<\/em> \u2013 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 \u2013 quite annoying even though it is just an exercise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We make (very) few modifications in <em>User Manager<\/em>. First, we modify <code>LoginQueryReply<\/code> to archive itself whenever a new user requests to register. We also override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRestore()\">onRestore()<\/a> to execute restored registration requests. Here is how it looks like:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.userManager;\n\nimport org.spiderwiz.tutorial.objectLib.LoginQuery;\n\n\/**\n * A class that replies LoginQuery queries\n *\/\npublic class LoginQueryReply extends LoginQuery {\n\n    \/**\n     * Examine the query fields and reply accordingly\n     * @return true because all queries are replied.\n     *\/\n    @Override\n    protected boolean onInquire() {\n        try {\n            RegisteredUser user;\n            if (isNewUser()) {\n                \/\/ A new user is added if a user with the same name does not exist, otherwise return an error code.\n                user = registerUser();\n                setResult(user == null ? ResultCode.OK : ResultCode.EXISTS);\n                \/\/ If registered successfully then archive it.\n                if (user == null)\n                    archive();\n            } else {\n                \/\/ If an existing user is loging in check password and return user details.\n                user = UserManagerMain.getInstance().getUser(getName());\n                if (user == null)\n                    setResult(ResultCode.NOT_EXISTS);\n                else if (!user.getPassword().equals(getPassword()))\n                    setResult(ResultCode.WRONG_PASSWORD);\n                else {\n                    setResult(ResultCode.OK);\n                    \/\/ Name is returned even though it is known because the name used when registering\n                    \/\/ may be different in case from the name used for login.\n                    setName(user.getName());\n                    setBirthday(user.getBirthday());\n                }\n            }\n        } catch (Exception ex) {\n            UserManagerMain.getInstance().sendExceptionMail(ex, &quot;When trying to archive a LogQuery item&quot;, null, false);\n        }\n        return true;\n    }\n\n    \/**\n     * @return the path pattern of the registration archive.\n     *\/\n    @Override\n    protected String getArchivePath() {\n        return &quot;register\/#y#m#d\/#h&quot;;\n    }\n\n    \/**\n     * Recover a query that is restored from the archive.\n     * @param associated    n\/a. contains null.\n     * @return\n     *\/\n    @Override\n    protected boolean onRestore(Object associated) {\n        registerUser();\n        return true;\n    }\n    \n    \/**\n     * Add a supposedly new user to the user table\n     * @return the previous user record with this name if exists, if it does not then return null.\n     *\/\n    private RegisteredUser registerUser() {\n        return UserManagerMain.getInstance().addUser(\n            new RegisteredUser(getName(), getPassword(), getBirthday())\n        );\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">You can see that within <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/QueryObject.html#onInquire()\">onInquire()<\/a> we call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#archive()\">archive()<\/a> on every successful registration request (line 23). You can also see that we override <code>getArchivePath()<\/code> (line 50) to return a path pattern that consists of <code>register<\/code> as a sub-folder, under which there is a folder for each day in the pattern <code>yymmdd<\/code>, under which there is an archive file for each hour of the day with the default <code>.arc<\/code> extension.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We split the code of <code>onInquire()<\/code> and move the registration part to the private <code>registerUser()<\/code> method, so that we can call it form either <code>onInquire()<\/code> or <code>onRestore()<\/code> (line 61). That\u2019s it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other change is in <code>UserManagerMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson8.userManager;\n\nimport java.util.List;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.lesson7.userManager.*;\nimport org.spiderwiz.tutorial.objectLib.ActiveUserQuery;\nimport org.spiderwiz.tutorial.objectLib.ConsoleMain;\nimport org.spiderwiz.tutorial.objectLib.LoginQuery;\nimport org.spiderwiz.zutils.ZDate;\nimport org.spiderwiz.zutils.ZHashMap;\n\n\/**\n * Provides the entry point of the application. Initializes and executes the Spiderwiz framework.\n *\/\npublic class UserManagerMain extends ConsoleMain {\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String CONF_FILENAME = &quot;user-manager.conf&quot;;\n    private static final String APP_NAME = &quot;User Manager&quot;;\n    private static final String APP_VERSION = &quot;Z1.01&quot;;  \/\/ Version Z1.01: Initial version\n\n    private static final String LIST = &quot;list&quot;;      \/\/ list all users currently in any room\n    \n    private final ZHashMap&lt;String, RegisteredUser&gt; users;\n    \n    \/**\n     * Class constructor with constant parameters. Call super constructor and create the user map.\n     *\/\n    public UserManagerMain() {\n        super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION);\n        users = new ZHashMap&lt;&gt;();\n    }\n    \n    \/**\n     * @return the class instance as UserManagerMain type.\n     *\/\n    public static UserManagerMain getInstance() {\n        return (UserManagerMain)ConsoleMain.getInstance();\n    }\n\n    \/**\n     * Application entry point. Instantiate the class and initialize the instance.\n     * \n     * @param args the command line arguments. Not used in this application.\n     *\/\n    public static void main(String[] args) {\n        new UserManagerMain().init();\n    }\n\n    \/**\n     * Print out usage instructions at start up\n     * @return true\n     *\/\n    @Override\n    protected boolean preStart() {\n        System.out.printf(&quot;To list all users currently in any room type 'list'.n&quot;);\n        return true;\n    }\n\n    \/**\n     * @return the list of produced object - ActiveUserQuery\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            ActiveUserQuery.ObjectCode\n        };\n    }\n\n    \/**\n     * @return the list of consumed object - LoginQuery\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{\n            LoginQuery.ObjectCode\n        };\n    }\n\n    \/**\n     * Add implementation classes to the object factory list of this application.\n     * @param factoryList\n     *\/\n    @Override\n    protected void populateObjectFactory(List&lt;Class&lt;? extends DataObject&gt;&gt; factoryList) {\n        super.populateObjectFactory(factoryList);\n        factoryList.add(LoginQueryReply.class);\n        factoryList.add(ActiveUserQueryInquire.class);\n    }\n    \n    \/**\n     * Process an input line\n     * @param line      An input line that contains a room management command\n     * @return true if processed successfully\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        try {\n            if (line.toLowerCase().startsWith(LIST))\n                listAllUsers();\n            return true;\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Theoretically arriving here if any data object does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            sendExceptionMail(ex, &quot;Cannot instantiate a data object class&quot;, null, false);\n            return false;\n        }\n    }\n\n    \/**\n     * Called after calling Main.init(). Restore user registration context\n     * @return true if restoration succeeded.\n     *\/\n    @Override\n    protected boolean postInit() {\n        return LoginQuery.restore(LoginQuery.ObjectCode, null, null, null) &gt;= 0;\n    }\n    \n    \/**\n     * Post an ActiveUserQuery query to get all active users\n     * @throws NoSuchFieldException     If any of the referred data objects does not define ObjectCode static field.\n     * @throws IllegalAccessException   If ObjectCode is not public.\n     *\/\n    private void listAllUsers() throws NoSuchFieldException, IllegalAccessException {\n        ActiveUserQuery query = createQuery(ActiveUserQuery.class);\n        query.post(5 * ZDate.SECOND);\n    }\n\n    \/**\n     * Return the RegisteredUser object associated with the user with the given name\n     * @param name          name of the user to look for\n     * @return a RegisteredUser object or null if the user does not exist\n     *\/\n    public RegisteredUser getUser(String name) {\n        return users.get(name.toLowerCase());\n    }\n    \n    \/**\n     * If the user does not exist add the given RegisteredUser to the user map.\n     * @param user  a RegisteredUser object to insert\n     * @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.\n     *\/\n    public RegisteredUser addUser(RegisteredUser user) {\n        return users.putIfAbsent(user.getName().toLowerCase(), user);\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here we override <code>postInit()<\/code> (line 114) to call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#restore(java.lang.String,org.spiderwiz.zutils.ZDate,org.spiderwiz.zutils.ZDate,java.lang.String...)\">restore()<\/a> in order to activate restoration of the archived <code>LoginQuery<\/code> messages. That completes our mission.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>archive folder<\/code> property is added to <code>user-manager.conf<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/UserManager\/Logs\n[archive folder]\/tests\/UserManager\/Archive\n[producer-1]ip=localhost;port=10001<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>A word about performance<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We claimed in the introduction to this lesson that the common archiving mechanism \u201chas a price tag of development time and performance\u201d, 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:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>The archived data is stored in sequential files, which are a lot faster than relational databases.<\/li><li>Unless the name extension of the archived files is <code>.text<\/code> or <code>.txt<\/code> (in our case we chose the default <code>.arc<\/code>), the files are compressed (with GZip).<\/li><li>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.<\/li><li>There is never more than one archive file opened at a time.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">In the <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-9\/\">next lesson<\/a> we will talk about <em>Manual Data Object Reset<\/em>, a topic that will bring us very close to full coverage of the <em>Spiderwiz Data Object Model<\/em>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Get a twofer. Learn about Streaming Queries and Data Object Archiving in one  lesson. We will use it to add a chat history feature to the chat service of the previous lessons.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":145,"menu_order":8,"comment_status":"closed","ping_status":"closed","template":"tutorial-child.php","meta":{"footnotes":""},"class_list":["post-898","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/898","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/comments?post=898"}],"version-history":[{"count":33,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/898\/revisions"}],"predecessor-version":[{"id":1421,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/898\/revisions\/1421"}],"up":[{"embeddable":true,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/145"}],"wp:attachment":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/media?parent=898"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}