{"id":1014,"date":"2020-08-13T20:30:28","date_gmt":"2020-08-13T20:30:28","guid":{"rendered":"http:\/\/spiderwiz.org\/project\/?page_id=1014"},"modified":"2025-04-09T06:16:13","modified_gmt":"2025-04-09T06:16:13","slug":"lesson-18","status":"publish","type":"page","link":"https:\/\/spiderwiz.org\/project\/tutorial\/lesson-18\/","title":{"rendered":"Lesson 18: Customizing SpiderAdmin"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><a href=\"http:\/\/spiderwiz.org\/project\/spideradmin\/\">SpiderAdmin<\/a> was introduced in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-3\/\">Lesson 3<\/a>\nof this tutorial and explained in detail in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-14\/\">Lesson 14<\/a>\nas a powerful tool for monitoring and administering a service mesh without\nadding a single line of code. The features we presented included data\ncommunication monitoring, configuration, log system exploration and some\ngeneral maintenance aspects. In this lesson we are going to learn how, in very\nfew lines of code, the tool can be extended to cover the logics of specific\napplications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Our test case will be, what else, the Chat application that\nwe have been dealing with along this tutorial. We will add the following\nelements to its SpiderAdmin page:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Two extra columns in the <em>Server Information<\/em>\ntable that show the current user name and the current room the user is chatting\nin (if any).<\/li><li>An extra table \u2013 <em>Room Activity<\/em>\n\u2013 that shows the activity of all the chatters in the room.<\/li><li>A \u201cBan Room\u201d button that the system administrator can use\nto kick out and\nban the user from\nthe room.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Before dealing with the required modifications to the Chat\napplication, let\u2019s put a small change in the <code>ChatApp<\/code> class of the <code>objectLib<\/code>\npackage:<\/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;\n\n\/**\n * A DataObject class that represents a chat application within a specific chat room. It is the parent of all the chatters that\n * are in that application and joined the room that is the parent of this object.\n *\/\npublic class ChatApp extends DataObject {\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n    public final static String ObjectCode = &quot;CHTAPP&quot;;\n    \n    @WizField private boolean banned = false;\n\n    public boolean isBanned() {\n        return banned;\n    }\n\n    public void setBanned(boolean banned) {\n        this.banned = banned;\n    }\n\n    @Override\n    protected String getParentCode() {\n        return ChatRoom.ObjectCode;\n    }\n\n    @Override\n    protected boolean isDisposable() {\n        return false;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We add a property \u2013 a boolean <code>banned<\/code> field (line 16) \u2013 that is set to <code>true<\/code> when the application instance (identified by its UUID) is\nbanned from the parent room. We will see below how it works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Back to the Chat application, 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.lesson18.chat;\n\nimport java.io.PrintStream;\nimport java.text.ParseException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport org.spiderwiz.admin.data.OpResults;\nimport org.spiderwiz.admin.data.PageInfo;\nimport org.spiderwiz.admin.data.PageInfo.TableInfo.Style;\nimport org.spiderwiz.admin.data.TableData;\nimport org.spiderwiz.core.CatchAllFilter;\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;Z18.01&quot;;  \/\/ Version Z18.01: Lesson 18 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    private static final String CURRENT_USER_TITLE = &quot;Current user&quot;;\n    private static final String CURRENT_ROOM_TITLE = &quot;Current room&quot;;\n    private static final String CHATTER_TABLE_TITLE = &quot;Room Activity&quot;;\n    private static final String CHATTER_TABLE_TAG = &quot;chatters&quot;;\n    private static final String BAN_BUTTON_LABEL = &quot;Ban Room&quot;;\n    private static final String BAN_BUTTON_TAG = &quot;ban&quot;;\n    private class ChatterTableColumnTitles {\n        static final String NAME = &quot;Chatter name&quot;;\n        static final String UUID = &quot;Application UUID&quot;;\n        static final String JOINED = &quot;Joined since&quot;;\n        static final String LAST = &quot;Last message at&quot;;\n        static final String COUNT = &quot;Message count&quot;;\n        static final String VOLUME = &quot;Message volume&quot;;\n        class SubTitles {\n            static final String COUNT_SINCE = &quot;Since last join&quot;;\n            static final String VOLUME_SINCE = &quot;In bytes since last join&quot;;\n        }\n    }\n\n    \/**\n     * Add a &quot;Current room&quot; title to the &quot;Server information&quot; SpiderAdmin table.\n     * @return the update table structure descriptor.\n     *\/\n    @Override\n    public PageInfo.TableInfo getServerInfoTableStructure() {\n        PageInfo.TableInfo tableInfo = super.getServerInfoTableStructure();\n        return tableInfo.\n            addColumn(CURRENT_USER_TITLE, null, 0, 0).\n            addColumn(CURRENT_ROOM_TITLE, null, 0, 0);\n    }\n\n    @Override\n    public TableData getServerInfoTableData() {\n        TableData data = super.getServerInfoTableData();\n        data.getRowData().get(0).\n            addCell(CURRENT_USER_TITLE, myName, 0, null).\n            addCell(CURRENT_ROOM_TITLE, chatter == null ? null : chatter.getRoomName(), 0, null);\n        return data;\n    }\n\n    \/**\n     * Add a &quot;Ban&quot; button and a &quot;Chatters&quot; table to the SpiderAdmin page layout.\n     * @param userID    N\/A\n     * @return the extended PageInfo object.\n     *\/\n    @Override\n    public PageInfo getPageInfo(String userID) {\n        PageInfo.TableInfo table = new PageInfo.TableInfo(CHATTER_TABLE_TITLE, CHATTER_TABLE_TAG, false);\n        table.\n            addColumn(ChatterTableColumnTitles.NAME, null, PageInfo.TableInfo.Style.NONE, PageInfo.TableInfo.Summary.NONE).\n            addColumn(ChatterTableColumnTitles.UUID, null, PageInfo.TableInfo.Style.NONE, PageInfo.TableInfo.Summary.NONE).\n            addColumn(ChatterTableColumnTitles.JOINED, null, PageInfo.TableInfo.Style.NONE, PageInfo.TableInfo.Summary.NONE).\n            addColumn(ChatterTableColumnTitles.LAST, null, PageInfo.TableInfo.Style.NONE, PageInfo.TableInfo.Summary.NONE).\n            addColumn(\n                ChatterTableColumnTitles.COUNT,\n                ChatterTableColumnTitles.SubTitles.COUNT_SINCE,\n                PageInfo.TableInfo.Style.RIGHT,\n                PageInfo.TableInfo.Summary.TOTAL\n            ).\n            addColumn(\n                ChatterTableColumnTitles.VOLUME,\n                ChatterTableColumnTitles.SubTitles.VOLUME_SINCE,\n                PageInfo.TableInfo.Style.RIGHT,\n                PageInfo.TableInfo.Summary.TOTAL\n            );\n        return super.getPageInfo(userID).addButton(BAN_BUTTON_LABEL, BAN_BUTTON_TAG, true).addTable(table);\n    }\n\n    \/**\n     * Execute the operation of the &quot;Ban&quot; button or return data for the &quot;Chatters&quot; table.\n     * @param serviceTag    the tag that identifies the button or the table.\n     * @param userID        N\/A\n     * @return an OpResults object in the case of a button or a TableData object in the case of a table.\n     *\/\n    @Override\n    public Object customAdminService(String serviceTag, String userID) {\n        switch(serviceTag) {\n        case CHATTER_TABLE_TAG:\n            return getChattersTableData();\n        case BAN_BUTTON_TAG:\n            return banUser();\n        }\n        return null;\n    }\n\n    \/**\n     * Get the data for the Chatters table\n     * @return a TableData object.\n     *\/\n    private TableData getChattersTableData() {\n        TableData data = new TableData();\n        if (chatter != null) {\n            try {\n                \/\/ Get all chatters in this room and sort them by name.\n                ChatRoom room = (ChatRoom)chatter.getParent().getParent();\n                List&lt;ChatterConsume&gt; users = room.getFilteredChildren(new CatchAllFilter&lt;&gt;(ChatterConsume.class));\n                Collections.sort(users);\n                \n                \/\/ populate the table\n                users.forEach((user) -&gt; {\n                    int style = user == chatter ? Style.ALERT : Style.NONE;     \/\/ Show our line in red\n                    String uuid = user.getParent().getObjectID();               \/\/ The parent is ChatterApp that is identifed by UUID\n                    data.addRow().\n                        addCell(\n                            ChatterTableColumnTitles.NAME,\n                            user.getName(),\n                            style,\n                            null\n                        ).addCell(\n                            ChatterTableColumnTitles.UUID,\n                            uuid,\n                            style,\n                            &quot;xadmin:&quot; + uuid    \/\/ this links to the SpiderAdmin page of the referred chatting application\n                        ).addCell(\n                            ChatterTableColumnTitles.JOINED,\n                            user.getJoined(),\n                            style,\n                            null\n                        ).addCell(\n                            ChatterTableColumnTitles.LAST,\n                            user.getLastMessageTime(),\n                            style,\n                            null\n                        ).addCell(\n                            ChatterTableColumnTitles.COUNT,\n                            user.getMessageCount(),\n                            style,\n                            null\n                        ).addCell(\n                            ChatterTableColumnTitles.VOLUME,\n                            user.getMessageVolume(),\n                            style,\n                            null\n                        );\n                });\n            } catch (NoSuchFieldException | IllegalAccessException ex) {\n                sendExceptionMail(ex, &quot;Exception when collectiing chatters&quot;, null, false);\n            }\n        }\n        return data;\n    }\n    \n    \/**\n     * Execute the operation of the Ban Room button\n     * @return an OpResults object.\n     *\/\n    private OpResults banUser() {\n        \/\/ Check if user in a room\n        if (chatter == null)\n            return new OpResults(&quot;User is not in a room&quot;);\n        System.out.printf(&quot;You are banned from room %sn&quot;, chatter.getRoomName());\n        \n        \/\/ Remove the room so that it cannot be joined again.\n        ChatApp chatApp = (ChatApp)chatter.getParent();\n        chatApp.getParent().remove();\n        \n        \/\/ Make sure the user will not see this room again.\n        chatApp.setBanned(true);\n        chatApp.commit();\n        \n        \/\/ Kick the user out of the room\n        leaveRoom(null);\n        \n        return new OpResults(OpResults.OK);\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                chatter.setMessageInfo(line);   \/\/ Update chatter statistics\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        chatter.setJoined();    \/\/ Update chatter statistics\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\">The SpiderAdmin customization code starts at line 152, where\nstring constants that are used for titles and labels are declared.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#getServerInfoTableStructure()\">getServerInfoTableStructure()<\/a> is overridden (line\n176) to add the two new columns to the layout of the <em>Server Information<\/em> table. The contents of the new cells (identified\nby column titles) are provided in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#getServerInfoTableData()\">getServerInfoTableData()<\/a> (line 184).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#getPageInfo(java.lang.String)\">getPageInfo()<\/a> is overridden (line 198) to add the\n<em>Ban Room<\/em> button and the <em>Room Activity<\/em> table to the layout of the\napplication&#8217;s page.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last method that we need to override is <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#customAdminService(java.lang.String,java.lang.String)\">customAdminService()<\/a> (line 227), where we check\nthe <code>serviceTag<\/code> argument (taken from\nthe values provided by <code>getPageInfo()<\/code>)\nand switch accordingly to either executing the button function or providing the\ndata for the new table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The content of the new table is provided by <code>getChattersTableData()<\/code> (line 241).\u00a0 The data is generated by applying the <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#getFilteredChildren(org.spiderwiz.core.Filter)\">getFilteredChildren()<\/a> method with a <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/CatchAllFilter.html\">CatchAllFilter<\/a> on the <code>ChatRoom<\/code> object that represents the current room to retrieve a list of all its chatters and then sort the list by user name (we will see below how <code>Chatter<\/code> is modified in order to enable the sort). We then use each <code>ChatterConsume<\/code> object of the sorted list to fill in the content of one table row. The code is pretty straightforward but there are few points that deserve special attention:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, we want to display the table row that represents the\ncurrent user in red. This is achieved by setting\nthe <code>style<\/code> argument of <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/admin\/data\/TableData.RowData.html#addCell(java.lang.String,java.lang.Object,int,java.lang.String)\">addCell()<\/a> to <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/admin\/data\/PageInfo.TableInfo.Style.html#ALERT\">Style.ALERT<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Secondly, note that some data cells contain time values. The\ndata for these cells is provided as <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/util\/Date.html\"><code>Date<\/code><\/a>\nobjects. SpiderAdmin is smart enough to display these values in the locale of\nthe browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lastly, note that when inserting cell data in the <em>Application UUID<\/em> column we provide a <code>uri<\/code> argument that consists of the UUID\npreceded by <code>\"xadmin:\"<\/code>. This\ntrick causes SpiderAdmin to show the UUID value as a link to the SpiderAdmin\npage of the linked application!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <em>Ban Room<\/em>\nbutton is handled in <code>banUser()<\/code> (line\n298). The method prints a message, sets the <code>banned<\/code>\nfield of the related <code>ChatApp<\/code> object\nto <code>true<\/code>, commits the object and then\ncalls <code>leaveRoom()<\/code> to leave the room.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other modifications to <code>ChatMain<\/code> are the call to <code>ChatterConsume.setMessageInfo()<\/code>\n(line 385) when the user types a message and to <code>ChatterConsume.setJoined()<\/code> (line 603) when the user joins a room.\nWe will see these methods in a moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As we have just mentioned, the data for the <em>Room Activity<\/em> table is taken from <code>ChatterConsume<\/code> objects. Here is the\nclass:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson18.chat;\n\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * Consumer implementation of Chatter data object. Notify when a chatter enters or leaves my room.\n *\/\npublic class ChatterConsume extends Chatter implements Comparable&lt;ChatterConsume&gt; {\n    private ZDate joined = null;\n    private ZDate lastMessageTime = null;\n    private int messageCount = 0;\n    private int messageVolume = 0;\n    \n    \/**\n     * Notify when a chatter enters my room.\n     *\/\n    @Override\n    protected void onNew() {\n        if (!isStealth() &amp;&amp; ChatMain.getInstance().isSameRoom(getRoomName()) &amp;&amp; getName() != null)\n            System.out.printf(&quot;%s entered the roomn&quot;, getName());\n        setJoined();\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(getParent().getObjectID()))\n            ChatMain.getInstance().leaveRoom(getRoomName());\n        else if (!isStealth() &amp;&amp; ChatMain.getInstance().isSameRoom(getRoomName()) &amp;&amp; getName() != null)\n            System.out.printf(&quot;%s left the roomn&quot;, getName());\n        return true;\n    }\n    \n    \/**\n     * Utility function to get the room name, which is the ID of the grand parent of the current object.\n     * @return the room name\n     *\/\n    String getRoomName() {\n        return getParent().getParent().getObjectID();\n    }\n\n    \/**\n     * @return the time the chatter joined the room.\n     *\/\n    public synchronized ZDate getJoined() {\n        return joined;\n    }\n\n    \/**\n     * Sets the &quot;joined&quot; time to now.\n     *\/\n    public void setJoined() {\n        this.joined = ZDate.now();\n    }\n\n    \/**\n     * @return the time of the last message of this chatter in the room.\n     *\/\n    public synchronized ZDate getLastMessageTime() {\n        return lastMessageTime;\n    }\n\n    \/**\n     * @return the number of messages that the sender sent in the room since joining.\n     *\/\n    public synchronized int getMessageCount() {\n        return messageCount;\n    }\n\n    \/**\n     *\n     * @return the volume of the messages in bytes that the sender sent in the room since joining.\n     *\/\n    public synchronized int getMessageVolume() {\n        return messageVolume;\n    }\n    \n    \/**\n     * Update statistics when a message from this chatter arrives in the room.\n     * @param message\n     *\/\n    public synchronized void setMessageInfo(String message) {\n        lastMessageTime = ZDate.now();\n        ++messageCount;\n        messageVolume += message.length();\n    }\n\n    \/**\n     * Compare objects by comparing the name property.\n     * @param o the object to compare to.\n     * @return negative value if this object is the lower, zero if they are equal and a positive value if this object is the higher.\n     *\/\n    @Override\n    public int compareTo(ChatterConsume o) {\n        String myName = getName();\n        String herName = o.getName();\n        return myName == null ? herName == null ? 0 : -1 : herName == null ? 1 : myName.compareTo(herName);\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The first thing that you can notice is that the class now\nimplements <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/lang\/Comparable.html\">Comparable<\/a>, so that lists of this class are\nsortable as we saw above. We also add four properties &#8211;&nbsp; <code>joined<\/code>,\n<code>lastMessageTime<\/code>, <code>messageCount<\/code> and <code>messageVolume<\/code>\nthat hold the information that we need for the <em>Room Activity <\/em>table. These properties have <code>getters<\/code> in lines 49, 63, 70 and 78 correspondingly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onNew()\">onNew()<\/a> is called when a chatter joins the\ncurrent room. In this method we add a call to <code>setJoined()<\/code> (line 22) that saves the join time in the <code>joined<\/code> property. We also add the <code>setMessageInfo()<\/code> method that is called\nwhen a message is typed in the room. The message is provided as the method\nargument and is used to update object properties as needed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"https:\/\/docs.oracle.com\/en\/java\/javase\/13\/docs\/api\/java.base\/java\/lang\/Comparable.html#compareTo%28T%29\">compareTo()<\/a> is implemented (line 98) to compare\nclass objects by comparing their <code>name<\/code>\nproperties.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other class that we need to modify is <code>ChatMessageImp<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson18.chat;\n\nimport java.util.Map;\nimport java.util.UUID;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\n\n\/**\n * Consumer implementation of the ChatMessage class\n *\/\npublic class ChatMessageImp extends ChatMessage {\n    \/**\n     * Called when a chat message is received. Check if the message has been sent in the room we are in,\n     * then print the sender name and the message. Also update chatter's statistics.\n     * @return true to indicate that the event has been handled.\n     *\/\n    @Override\n    protected boolean onEvent() {\n        if (ChatMain.getInstance().isSameRoom(getParent().getParent().getParent().getObjectID())) {\n            ChatterConsume chatter = (ChatterConsume)getParent();\n            String name = chatter.getName();\n            if (name == null)\n                name = chatter.getObjectID();\n            System.out.printf(&quot;%1$s: %2$sn&quot;, name, getMessage());\n            chatter.setMessageInfo(getMessage());\n        }\n        return true;\n    }\n\n    \/**\n     * Restrict message sending to:\n     *      1. applications that are not the current application.\n     *      2. applications that are on the same chat room as the current user, or:\n     *      3. applications whose 'appParams' contains the mapping 'role'-&gt;'admin'\n     * @param appUUID           destination application UUID\n     * @param appName           destination application name\n     * @param userID            destination user ID\n     * @param remoteAddress     remote network address\n     * @param appParams         application parameters of the destination application\n     * @return true if the object should be sent to this destination\n     *\/\n    @Override\n    protected boolean filterDestination(UUID appUUID, String appName, String userID, String remoteAddress,\n        Map&lt;String, String&gt; appParams\n    ) {\n        return\n            !ChatMain.getInstance().getAppUUID().equals(appUUID) &amp;&amp; (\n                isMemberOfMyRoom(appUUID) || appParams != null &amp;&amp; &quot;admin&quot;.equals(appParams.get(&quot;role&quot;))\n            );\n    }\n\n    \/**\n     * Check if the user running the application whose UUID is given is on the same room as this message is sent to.\n     * @param appUUID   Application UUID to check\n     * @return  true if the application is in the same room as us.\n     *\/\n    private boolean isMemberOfMyRoom(UUID appUUID) {\n        try {\n            \/\/ The room is the great-grandparent of the message\n            ChatRoom chatRoom = (ChatRoom)getParent().getParent().getParent();\n            return chatRoom.getChild(ChatApp.class, appUUID.toString()) != null;\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Arriving here if a ChatApp does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            ChatMain.getInstance().sendExceptionMail(ex, &quot;Cannot instantiate ChatApp&quot;, null, false);\n            return false;\n        }\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We add code in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onEvent()\">onEvent()<\/a> (line 19) that updates the parent <code>ChatterConsume<\/code> object when a message is\nreceived.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We are almost done but we have a little problem. The changes\nthat we made to the Chat application basically did the job. I.e. when the\nadministrator clicks the <em>Ban Room<\/em>\nbutton on the application\u2019s page the user is kicked out of the room and the <code>ChatRoom<\/code> object is deleted so that the\nuser cannot join the room again. But what if the application restarts or\nreconnects to the network? In these cases the application, as a consumer of <code>ChatRoom<\/code>, requests a reset for this\nobject type and gets all available rooms including the banned one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here comes to rescue the <code>banned<\/code>\nproperty that we added to <code>ChatApp<\/code>. We\nwill show its use with the <em>Room Manager<\/em>\napplication which was introduced in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-5\/\">Lesson 5<\/a>.\nWe will use it to restrict the routing of <code>ChatRoom<\/code>\nobjects so that applications that are banned from a room will not get it. Here\nis <code>RoomManagerMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson18.roomManager;\n\nimport java.util.List;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\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 RoomManagerMain extends ConsoleMain {\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String CONF_FILENAME = &quot;room-manager.conf&quot;;\n    private static final String APP_NAME = &quot;Room Manager&quot;;\n    private static final String APP_VERSION = &quot;Z18.01&quot;;  \/\/ Version Z18.01: Lesson 18 version\n\n    private static final String CREATE = &quot;create&quot;;  \/\/ create chat room command\n    private static final String DELETE = &quot;delete&quot;;  \/\/ delete chat room command\n    private static final String MODIFY = &quot;modify&quot;;  \/\/ modify chat room command\n    private static final String RENAME = &quot;rename&quot;;  \/\/ rename chat room command\n    \n    \/**\n     * Class constructor with constant parameters.\n     *\/\n    public RoomManagerMain() {\n        super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION);\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 RoomManagerMain().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(\n            &quot;To create a new room: type 'create &lt;room name&gt;'.n&quot;\n                + &quot;Append a plus sign (+) to the name if you want to create an adult only room.n&quot;\n                + &quot;To delete a room type 'delete &lt;room name&gt;'.n&quot;\n                + &quot;To modify the adult categorization of a room:n&quot;\n                + &quot;Type 'modify &lt;room name&gt;+' if you want to make it an adult room.n&quot;\n                + &quot;Type 'modify &lt;room name&gt;' if you want to remove the adult categorization.n&quot;\n                + &quot;To rename a room type 'rename &lt;room name&gt;=&lt;new name&gt;'.n&quot;\n                + &quot;To exit type 'exit'.n&quot;\n        );\n        return true;\n    }\n\n    \/**\n     * @return the list of produced objects, Chat is the only one.\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            ChatRoom.ObjectCode\n        };\n    }\n\n    \/**\n     * @return the list of consumed objects, we need ChatApp for banning certain applications from certain rooms.\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{\n            ChatApp.ObjectCode\n        };\n    }\n\n    \/**\n     * Add ChatRoom class 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(ChatRoomProduce.class);\n        factoryList.add(ChatAppConsume.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            \/\/ Trim line and split to command \/ parameter.\n            \/\/ Then check if there is a parameter and whether it contains the adult symbol.\n            \/\/ If there is not parameter, ignore the command.\n            String command[] = line.trim().split(&quot;s+&quot;, 2);\n            if (command.length &lt; 2)\n                return true;\n            String name[] = command[1].split(&quot;+&quot;, -1);\n            boolean adult = name.length &gt; 1;\n            \/\/ now split for 'rename' command\n            name = name[0].split(&quot;=&quot;);\n            String roomName = name[0];\n            \n            \/\/ If exists, get the ChatRoom object\n            ChatRoom room = getRootObject().getChild(ChatRoom.class, roomName);\n            \n            \/\/ Switch by command and process it\n            switch (command[0].toLowerCase()) {\n            case CREATE:\n                \/\/ Create a new room if does not exist (ignore if it does)\n                if (room == null) {\n                    room = createTopLevelObject(ChatRoom.class, roomName);\n                    room.setAdult(adult);\n                    room.commit();\n                    System.out.printf(&quot;Room %1$s(%2$s) createdn&quot;, roomName, adult ? &quot;adult&quot; : &quot;unrestricted&quot;);\n                }\n                break;\n            case DELETE:\n                \/\/ Delete the room if exists\n                if (room != null) {\n                    room.remove();\n                    room.commit();\n                    System.out.printf(&quot;Room %1$s deletedn&quot;, roomName);\n                }\n                break;\n            case MODIFY:\n                \/\/ Modify the 'adult' field if the room exits\n                if (room != null) {\n                    room.setAdult(adult);\n                    room.commit();\n                    System.out.printf(&quot;Room %1$s set to %2$sn&quot;, roomName, adult ? &quot;adult&quot; : &quot;unrestricted&quot;);\n                }\n                break;\n            case RENAME:\n                \/\/ Rename the room if exists. The rename command is 'rename &lt;old name&gt;=&lt;new name&gt;'\n                if (room != null &amp;&amp; name.length &gt; 1) {\n                    String newName = name[1];\n                    if (!newName.equalsIgnoreCase(roomName)) {\n                        DataObject renamed = room.rename(newName);\n                        if (renamed == null)\n                            System.out.printf(&quot;Cannot rename to already existing room name %sn&quot;, newName);\n                        else {\n                            renamed.commit();\n                            System.out.printf(&quot;Room %1$s was renamed %2$sn&quot;, roomName, newName);\n                        }\n                    }\n                }\n            }\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}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">There are two changes here. The application is now a\nconsumer of <code>ChatApp<\/code> (line 74) so that\nit is notified when an application instance is banned from a room. Additionally,\ntwo implementation classes \u2013 <code>ChatRoomProduce<\/code>\nand <code>ChatAppConsume<\/code> have been added to\nthe package and are registered in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#populateObjectFactory(java.util.List)\">populateObjectFactory()<\/a> (line 83).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is what we do in <code>ChatRoomProduce<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson18.roomManager;\n\nimport java.util.Map;\nimport java.util.UUID;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\n\n\/**\n * Extends ChatRoom to make sure the room is not published to banned users.\n *\/\npublic class ChatRoomProduce extends ChatRoom {\n\n    \/**\n     * Filter out banned users.\n     * @param appUUID       application UUID.\n     * @param appName       application name.\n     * @param userID        the user ID attached to the network channel through which the destination application is connected to the\n     *                      current application. \n     * @param remoteAddress remote address of the destination application.\n     * @param appParams     application parameter map as set by {@link org.spiderwiz.core.Main#getAppParams()}\n     *                      method of the destination application. May be null if the destination application did not define any\n     *                      parameters.\n     * @return              true to approve distribution to this application, false to deny it.\n     *\/\n    @Override\n    protected boolean filterDestination(UUID appUUID, String appName, String userID,\n        String remoteAddress, Map&lt;String, String&gt; appParams\n    ) {\n        try {\n            ChatApp chatApp = getChild(ChatApp.class, appUUID.toString());\n            return chatApp == null || !chatApp.isBanned();\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            RoomManagerMain.getInstance().sendExceptionMail(ex, &quot;Cannot instantiate ChatApp&quot;, null, false);\n            return false;\n        }\n    }\n}\n<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#filterDestination(java.util.UUID,java.lang.String,java.lang.String,java.lang.String,java.util.Map)\">filterDestination()<\/a> is overridden (line 26) to\nfilter out banned application instances from receiving the banned rooms.\nSimple, no? Wait a moment! How do we know which instances are banned? From the <code>banned<\/code> property of the corresponding <code>ChatApp<\/code> object, right? But don\u2019t we kick\nout a user from a room by deleting the associated <code>ChatApp<\/code> object? If so, how can we get the <code>banned<\/code> property of a deleted object?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Well, the answer is in <code>ChatAppConsume<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson18.roomManager;\n\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\n\n\/**\n * Implements ChatApp to support persistent user ban\n *\/\npublic class ChatAppConsume extends ChatApp{\n\n    \/**\n     * If the application is marked as &quot;banned&quot; prevent object removal so that the room (parent of this object) will not be sent\n     * to the application when it requests a reset.\n     * @return true if not banned, false if it is.\n     *\/\n    @Override\n    protected boolean onRemoval() {\n        return !isBanned();\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">All we need to do is to override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRemoval()\">onRemoval()<\/a> to return <code>false<\/code> if the <code>banned<\/code>\nproperty is <code>true<\/code>. That causes the\nframework to locally ignore the removal and keep the object in its space, so\nthat <code>ChatRoomProduce<\/code> can filter it\nout. Mission done!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a sample of the resulted Chat application page on\nSpiderAdmin:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"633\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/09\/lesson-18-1024x633.png\" alt=\"\" class=\"wp-image-1178\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/09\/lesson-18-1024x633.png 1024w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/09\/lesson-18-300x186.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/09\/lesson-18-768x475.png 768w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/09\/lesson-18.png 1279w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">With this cool stuff we are arriving to the end of this\ntutorial. End but not completed. There are more readings and activities that we\ncan recommend for those who want to become real Spiderwiz experts. More about\nit <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/where-do-we-go-from-here\/\">here<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to use <a href= http:\/\/spiderwiz.org\/project\/spideradmin\/>SpiderAdmin<\/a> for controlling and analyzing your application\u2019s logical functionality by adding application-specific features to SpiderAdmin pages.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":145,"menu_order":18,"comment_status":"closed","ping_status":"closed","template":"tutorial-child.php","meta":{"footnotes":""},"class_list":["post-1014","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1014","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=1014"}],"version-history":[{"count":11,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1014\/revisions"}],"predecessor-version":[{"id":1429,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/1014\/revisions\/1429"}],"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=1014"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}