{"id":1003,"date":"2020-08-13T20:07:27","date_gmt":"2020-08-13T20:07:27","guid":{"rendered":"http:\/\/spiderwiz.org\/project\/?page_id=1003"},"modified":"2025-04-09T06:15:58","modified_gmt":"2025-04-09T06:15:58","slug":"lesson-13","status":"publish","type":"page","link":"https:\/\/spiderwiz.org\/project\/tutorial\/lesson-13\/","title":{"rendered":"Lesson 13: Everything that Makes Maintenance Life Easier"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Experienced programmers know that developing a software\nproject that works is only half of their task, if not less. To do a full job,\nthey have to provide procedures and tools that can be used by maintenance and\nsupport staff when the project does not work as expected. This lesson is\ndedicated to features that will assist you with that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Logging<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Application logs are an indispensable part of every serious\nsoftware project. We saw how they came free with every Spiderwiz application\nright from the <em>Hello World<\/em> project of\n<a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-1\/\">Lesson\n1<\/a>. Spiderwiz logging system can go from basic events as we saw\nthere, to a full log of every transaction that goes over a communication line.\nWith respect to the latter, since the transactions are potentially <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/compression.html\">subject to compression<\/a>, logging can be configured\nto show compressed data, uncompressed data or both. For a thorough discussion\nof the topic see <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/logging.html\">Spiderwiz Logging System<\/a> in the project\u2019s\nJavadoc.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Alert messages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Spiderwiz Programming Model is about data communication. It\nis true that the model is designed to let programmers forget that point, but\nsystem administrators must get promptly aware of any communication breach or\napplication malfunction. This is done by sending alerts through email or other\nmeans.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Spiderwiz framework includes full support of system and\ncustom alert notifications. System alerts are triggered when an application\ndetects that a peer application is disconnected. There is also a \u201cback to\nroutine\u201d notification that is triggered when an application that was previously\ndisconnected connects again. The exact condition when and by which service\nnotifications are triggered is specified in application configuration file\nthrough the following properties and parameters:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>disconnection\nalert minutes<\/em><\/li><li><em>idle alert\nminutes<\/em><\/li><li><code>alert<\/code> parameter of <code>producer-<\/code><em>n<\/em>, <code>consumer-<\/code><em>n<\/em>, <code>producer server<\/code>&#8211;<em>n<\/em>, <code>consumer server<\/code>&#8211;<em>n<\/em>\nand <code>import-<\/code><em>n<\/em>.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For details please see <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/config.html\">Spiderwiz Configuration<\/a> in the project\u2019s Javadoc.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every notification is logged in the application\u2019s log\nsystem. Additionally you can configure a mail system that is used by the\nframework to send notification emails. Currently only SMTP mail is supported,\nbut you can implement a <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-16\/\">mail plugin<\/a> to support any messaging system that\nfits. Email sending is configured through the following properties:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><code>mail system<\/code><\/li><li><code>from address<\/code><\/li><li><code>to email<\/code><\/li><li><code>cc email<\/code><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These are also described in detail in the <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/config.html\">configuration Javadoc<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Custom alerts are triggered programmatically by calling <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#sendNotificationMail(java.lang.String,java.lang.String,org.spiderwiz.zutils.ZDate,boolean)\">sendNotificationMail()<\/a> and are logged and mailed\nin the same way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exception alerts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Code exceptions (i.e. bugs) are another kind of event that\nrequire prompt notification. While the addressees of system alerts are usually\nsystem administrators, exceptions should go to programmers. As with alerts,\nSpiderwiz has everything that you need to handle exception notifications\nefficiently.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Some exceptions are caught by the framework while others occur in your code or when you call a framework method that throws exceptions. In the first case, a notification is triggered by the framework. In the second case you can call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#sendExceptionMail(java.lang.Throwable,java.lang.String,java.lang.String,boolean)\">Main.sendExceptionMail()<\/a> to trigger a similar notification. In both cases the notification, which includes the exception details and stack trace, is logged in the application\u2019s logging system and is also sent by email if a mail system is configured. The addressees for such emails in the case of exceptions are configured by the <code>to exception email<\/code> and <em>cc exception email<\/em> properties.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note that <code>sendExceptionMail()<\/code> has a <code>boolean<\/code> parameter called <code>critical<\/code>. Every call to <code>sendExceptionMail()<\/code> with the parameter set to <code>true<\/code> triggers a notification. On the contrary when <code>critical<\/code> is set to <code>false<\/code>, notifications are not sent more frequently than the value configured by the <code>exception alert rate<\/code> property.  This lets you avoid email inundation for exceptions that are thrown over and over again.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Mail delegation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">It is often the case that production systems are behind a\nfirewall and do not have access to a mail server. It happens to be that this is\nalso the case when getting email alerts about system malfunction is the most\nimportant, because programming access to these systems is also limited, and so\nis the ability of maintenance staff to check what is going on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is another case where Spiderwiz offers a simple\nsolution to a thorny problem. The solution is called <em>Mail Delegation<\/em>, and it requires no more than one line of code in\neach involved application. The idea is that the application without mail server\naccess delegates the mission to an application that does have it. To make it\nwork, you would need to do the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>There is no need to configure mail system properties for\nthe application without mail server access.<\/li><li>Mail system properties are configured for the application\nthat has mail server access.<\/li><li>The application without email access includes <code>ObjectCodes.EventReport<\/code> in the list of\nobject types that it produces.<\/li><li>The application that has email access includes <code>ObjectCodes.EventReport<\/code> in the list of object types that it consumes.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s it! Let\u2019s give it a try with our chat system. We\nassume that we cannot guarantee that every running chat application has access\nto a mail server. However, we can assure that <em>My Hub<\/em>, which all the applications are connected through, has. So\nfirstly we modify <code>ChatMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson13.chat;\n\nimport java.io.PrintStream;\nimport java.text.ParseException;\nimport java.util.Collection;\nimport java.util.List;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.objectLib.ActiveUserQuery;\nimport org.spiderwiz.tutorial.objectLib.Chat;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\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;\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;Z13.01&quot;;  \/\/ Version Z13.01: Lesson 13 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 ChatterConsume 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     * @return the list of produced objects - Chatter, Chat, ChatMessage and LoginQuery\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            ChatApp.ObjectCode,\n            Chatter.ObjectCode,\n            Chat.ObjectCode,\n            ChatMessage.ObjectCode,\n            LoginQuery.ObjectCode,\n            ChatHistoryQuery.ObjectCode,\n            ObjectCodes.EventReport         \/\/ Added in Lesson 13\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            ChatApp.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(ChatApp.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.setTime(ZDate.now());\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.setTime(ZDate.now());\n                message.commit();\n            }\n        } catch (Exception ex) {\n            sendExceptionMail(ex, &quot;Exception when processing an input line&quot;, line, true);\n        }\n        return true;\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.getRoomName()))\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        \/\/ Join the application to the room\n        ChatApp chatApp = chatRoom.createChild(ChatApp.class, getAppUUID().toString());\n        \n        \/\/ Create a Chatter object for the user that joins the specified room\n        chatter = chatApp.createChild(ChatterConsume.class, myName);\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            \/\/ Leave the room and let everybody know I did\n            DataObject removed = chatter.remove();\n            if (removed != null)                    \/\/ can be null if the 'chatter' object has already been removed.\n                removed.commit();\n            \n            \/\/ Disconnect the application from the room and let every application know it did\n            removed = chatter.getParent().remove();\n            if (removed != null)\n                removed.commit();\n            System.out.printf(&quot;Left room %sn&quot;, chatter.getRoomName());\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.getRoomName()));\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.getRoomName();\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 the object ID of its grandparent, which is the UUID of the\n     *              application 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.getParent().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 = Integer.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\">In line 110 we add <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.ObjectCodes.html#EventReport\">ObjectCodes.EventReport<\/a> to the list returned by <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#getProducedObjects()\">getProducedObjects()<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In order to easily test an exception, we change the parsing\nof the <code>!history<\/code> command to use <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/lang\/Integer.html#parseInt%28java.lang.String%29\">Integer.parseInt()<\/a> instead of <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/zutils\/ZUtilities.html#parseInt(java.lang.String)\">ZUtilities.parseInt()<\/a> (line 504). We do it\nbecause the former throws an exception if its parameter is not a valid integer\nnumber, while the latter does not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And finally, in line 215, we catch every Exception type and\ncall <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#sendExceptionMail(java.lang.Throwable,java.lang.String,java.lang.String,boolean)\">sendExceptionMail()<\/a> to send an appropriate\nmessage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Back to <em>My Hub<\/em>, we\nmodify <code>HubMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson3;\n\nimport org.spiderwiz.core.Main;\n\n\/**\n * Main class for MyHub. As a hub, we neither produce nor consume any object.\n *\/\npublic class HubMain extends Main{\n    private static final String ROOT_DIRECTORY = &quot;\/tests\/MyHub&quot;;\n    private static final String CONFIG_FILE_NAME = &quot;hub.conf&quot;;\n    private static final String APP_NAME = &quot;My Hub&quot;;\n    private static final String APP_VERSION = &quot;Z13.01&quot;;  \/\/ Version Z13.01: Amend for lesson 13\n\n    public HubMain() {\n        super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION);\n    }\n\n    \/**\n     * @return an empty list as we produce nothing\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{};\n    }\n\n    \/**\n     * @return a list containing ObjectCodes.EventReport because this application can send mails.\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{ObjectCodes.EventReport};\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Just add <code>ObjectCodes.EventReport<\/code>\nto the list returned by <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#getConsumedObjects()\">getConsumedObjects()<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We also need to configure the mail system in <code>hub.conf<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]Logs\n[consumer server-1]websocket;ping-rate=30\n[producer server-1]websocket;ping-rate=30\n[consumer server-2]port=10001\n[spideradmin]iciDtSTUFzuD6+kno9TmiZtaFNlQ\n[hub mode]yes\n[mail system]smtp;server=smtp.gmail.com;user=tester@spiderwiz.org;pwd=dumptrump;port=465;ssl=true\n[from address]My Hub Alerts&lt;alert@spiderwiz.org&gt;\n[to email]Spiderwiz Administrator&lt;spiderwiz.admin@gmail.com&gt;\n[to exception email]Spiderwiz Programmer&lt;spiderwiz.geek@gmail.com&gt;<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Of course, use your own parameters instead of the ones used\nhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now run all the services (or at least <em>My Hub<\/em>, <em>User Manager<\/em> and\none chat application), login from a chat application and try to type:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>!history 1od<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">(\u201cby mistake\u201d you typed the letter \u2018o\u2019 instead of zero in <code>\u201d1od\u201d<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You get an exception, you see it on your console, and you will also get an email like this:<\/p>\n\n\n\n<div class=\"mailframe\">\n    <div class=\"mailheader\">\n        <p>\n            <b>From:<\/b>\n            My Hub Alerts[alert@spiderwiz.org]\n        <\/p>\n        <p>\n            <b>Sent:<\/b>\n            Wednesday, August 19, 2020 8:19\n        <\/p>\n        <p>\n            <b>To:<\/b>\n            Spiderwiz Programmer[spiderwiz.geek@gmail.com]\n        <\/p>\n        <p>\n            <b>Subject:<\/b>\n            An application exception in Chat 1 running at 192.168.1.5\n        <\/p>\n    <\/div>\n    <div>\n        <h1 style=\"color: red\">An application exception in Chat 1 running at 192.168.1.5<\/h1>\n        <h1 style=\"color: red\">Exception when processing an input line<\/h1>\n        <h3 style=\"\">Event time: 20\/08\/20-10:36:04:000<\/h3>\n        <h2 style=\"\">Sent by My Hub running at 192.168.1.5<\/h2>\n        <h3 style=\"\">Additional information:<\/h3>\n        <p>!history 1od<\/p>\n        <h3 style=\"\">Exception details:<\/h3>\n        <p>java.lang.NumberFormatException: For input string: &#8220;1o&#8221;\n            <br>     at java.base\/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)\n            <br>     at java.base\/java.lang.Integer.parseInt(Integer.java:658)\n            <br>     at java.base\/java.lang.Integer.parseInt(Integer.java:776)\n            <br>     at org.spiderwiz.tutorial.lesson13.chat.ChatMain.getHistory(ChatMain.java:504)\n            <br>     at org.spiderwiz.tutorial.lesson13.chat.ChatMain.processConsoleLine(ChatMain.java:180)\n            <br>     at org.spiderwiz.tutorial.objectLib.ConsoleMain.postStart(ConsoleMain.java:50)\n            <br>     at org.spiderwiz.core.Main.init(Main.java:411)\n            <br>     at org.spiderwiz.tutorial.lesson13.chat.ChatMain.main(ChatMain.java:69)\n        <\/p>\n    <\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">You get this email even though you did not define mail\nserver parameters in the chat application. As you can see in the email, it was\ntunneled through <em>My Hub<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Having done with the essentials of programming and debugging\nSpiderwiz applications, we will delve in the <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-14\/\">next\nlesson<\/a> into <strong>SpiderAdmin<\/strong> \u2013\nthe powerful management service that comes free with every Spiderwiz-based\nproject.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Logs, alerts, exception reporting, monitoring and everything that lets you focus on your tasks without spending your entire life on debugging.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":145,"menu_order":13,"comment_status":"closed","ping_status":"closed","template":"tutorial-child.php","meta":{"footnotes":""},"class_list":["post-1003","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1003","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=1003"}],"version-history":[{"count":21,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1003\/revisions"}],"predecessor-version":[{"id":1426,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1003\/revisions\/1426"}],"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=1003"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}