{"id":986,"date":"2020-08-09T09:48:34","date_gmt":"2020-08-09T09:48:34","guid":{"rendered":"http:\/\/spiderwiz.org\/project\/?page_id=986"},"modified":"2025-04-09T06:15:58","modified_gmt":"2025-04-09T06:15:58","slug":"lesson-11","status":"publish","type":"page","link":"https:\/\/spiderwiz.org\/project\/tutorial\/lesson-11\/","title":{"rendered":"Lesson 11: Import\/Export \u2013 Interfacing with Legacy Frameworks"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">After ten lessons you are probably excited about Spiderwiz\nas much as we are, but the framework has one drawback \u2013 not everybody uses it\nyet. When you develop with Spiderwiz, it is quite probable that you would need\nto interface with legacy systems that use different ways of data delivery.\nSpiderwiz Import-Export mechanism eases this process substantially.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The concept follows the framework paradigm that everything\nthat happens to a Data Object is encapsulated within its class code. Therefore\ndata objects import themselves and export themselves. In this lesson we will\nsee how it works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To demonstrate the topic we will use \u2013 guess what \u2013 our chat\nsystem of the previous lessons. Assuming there is another chat system,\nimplemented by other means, that accesses the same chat rooms. We want to\nbridge the two systems so that the activity of chatters that use one will be\nvisible to chatters that use the other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To simplify the demonstration, we assume that information\nthat passes between the two systems, let\u2019s say over a TCP\/IP socket, is in the\nform of text lines, each contains a message in one of the following formats:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>!join,&lt;room\nname&gt;,&lt;chatter name&gt;<\/em><\/li><li><em>!leave,&lt;room\nname&gt;&lt;chatter name&gt;<\/em><\/li><li><em>&lt;room\nname&gt;,&lt;chatter name&gt;,&lt;message&gt;<\/em><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The first is sent when a user joins a room, the second when\nthe user leaves it and the third when a user posts a message in a room.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We are going to add a new service to our mesh called <em>&#8220;Chat Room Bridge&#8221;<\/em>. Its role is to\nconnect to the foreign system, read lines in the said formats, parse them and\nfeed them to our system as native <code>Chatter<\/code>\nand <code>ChatMessage<\/code> objects. On the other\nhand the service listens to our chat rooms, converts <code>Chatter<\/code> and <code>ChatMessage<\/code>\nobjects to text lines of the above formats and transmits them to the foreign\nsystem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before starting, we need to tackle a small architectural\nproblem. In the previous lessons we had one chat application used by one user.\nThis let us identifying the user by the application UUID, which we used as a\ntrick to easily route messages to the proper destinations. This trick will not\nwork here, because we are going to have one application, the bridge, that\nrepresents all the users across it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The solution is to make a slight change in the hierarchy of\nour Data Object Tree. We will insert a new data object class, <code>ChatApp<\/code>, as a child of <code>ChatRoom<\/code> and a parent of <code>Chatter<\/code>. <code>ChatApp<\/code> has no properties and is identified by the application\nUUID. Its children of type <code>Chatter<\/code>\nare identified by the user name. As there can be multiple <code>Chatter<\/code> children to a single <code>ChatApp<\/code>\nobject, the problem is solved elegantly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With this change, <code>ChatRoom<\/code>\nremains as it is. The new <code>ChatApp<\/code>\nclass is just a barebones Data Object, defined as a non-disposable child of <code>ChatRoom<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.objectLib;\n\nimport org.spiderwiz.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    @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\">Here is the modified <code>Chatter<\/code>\nclass:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.objectLib;\n\nimport org.spiderwiz.annotation.WizField;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.zutils.ZDate;\n\n\/**\n * A data object that represents a chat user using a specific chat application in a specific chat room\n *\/\npublic class Chatter extends DataObject {\n    \/**\n     * Mandatory public static field for all data objects.\n     *\/\n    public final static String ObjectCode = &quot;CHTTR&quot;;\n    \n    @WizField private String name;              \/\/ Contains the name of the chatter\n    @WizField private ZDate birthday;           \/\/ Contains the chatter birth date\n    @WizField private boolean stealth = false;  \/\/ True if chatters shall not be notified when this user joins or leave a room\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public ZDate getBirthday() {\n        return birthday;\n    }\n\n    public void setBirthday(ZDate birthday) {\n        this.birthday = birthday;\n    }\n\n    public boolean isStealth() {\n        return stealth;\n    }\n\n    public void setStealth(boolean stealth) {\n        this.stealth = stealth;\n    }\n\n    \/**\n     * @return ChatRoom object code\n     *\/\n    @Override\n    protected String getParentCode() {\n        return ChatApp.ObjectCode;\n    }\n\n    \/**\n     * @return false because objects of this class are persistent\n     *\/\n    @Override\n    protected boolean isDisposable() {\n        return false;\n    }\n\n    \/**\n     * The object ID, which is the user name, is case insensitive.\n     * @return\n     *\/\n    @Override\n    protected boolean isCaseSensitive() {\n        return false;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The class is now defined (line 48) as a child of <code>ChatApp<\/code>. We also define it as case\ninsensitive (line 65) since the object ID is now the user name that we do not\nwant to bother with its case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is no change in <code>ChatMessage<\/code>\nand other library classes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The new structure requires few modifications to the chat\napplication. 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.lesson11.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;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Provides the entry point of the application. Initializes and executes the Spiderwiz framework.\n *\/\npublic class ChatMain extends ConsoleMain{\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String APP_NAME = &quot;Chat Room Client&quot;;\n    private static final String APP_VERSION = &quot;Z11.01&quot;;  \/\/ Version Z11.01: Lesson 11 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        };\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            return true;\n        } catch (NoSuchFieldException | IllegalAccessException ex) {\n            \/\/ Theoretically arriving here if a data object does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send a command exception message.\n            sendExceptionMail(ex, &quot;Cannot instantiate a data object class&quot;, null, false);\n            return false;\n        }\n    }\n    public String getMyName() {\n        return myName;\n    }\n\n    public ZDate getBirthday() {\n        return birthday;\n    }\n    \n    \/**\n     * Get user name and password and log in.\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     * @throws NoSuchFieldException     If LoginQuery does not define ObjectCode static field.\n     * @throws IllegalAccessException   If LoginQuery.ObjectCode is not public.\n     *\/\n    private PrintStream login() throws NoSuchFieldException, IllegalAccessException {\n        String username;\n        String password;\n\n        \/\/ Check if already logged in\n        if (myName != null)\n            return System.out.printf(&quot;You are already logged in as %s. To log in with another name log out first.n&quot;, myName);\n        \n        \/\/ Create a login query\n        LoginQuery query = createQuery(LoginQuery.class);\n        while(true) {\n            \/\/ Get user name\n            System.out.printf(&quot;(type Enter to exit login)n&quot;);\n            username = readConsoleLine(&quot;User name:&quot;);\n            if (username.isBlank())\n                return exitLogin();\n            do {\n                \/\/ Get password\n                password = readConsoleLine(&quot;Password:&quot;);\n                if (password.isBlank())\n                    return exitLogin();\n\n                \/\/ Post the login query\n                query.setNewUser(false);\n                query.setName(username);\n                query.setPassword(password);\n                query.post(5 * ZDate.SECOND);\n                \n                \/\/ If the query did not expire check login results, if it did, repeat.\n                if (query.waitForReply()) {\n                    switch (query.getResult()) {\n                    case OK:\n                        myName = query.getName();\n                        birthday = query.getBirthday();\n                        return System.out.printf(&quot;Logged in successfully as %s.n&quot;, myName);\n                    case NOT_EXISTS:\n                        System.out.printf(&quot;User %s does not exist. Please try again.n&quot;, username);\n                        break;\n                    case WRONG_PASSWORD:\n                        System.out.printf(&quot;Password did not match, please try again.n&quot;);\n                        break;\n                    }\n                } else\n                    return unavailableUserManager();\n            } while (query.getResult() == LoginQuery.ResultCode.WRONG_PASSWORD);\n        }\n    }\n    \n    \/**\n     * Log out if already logged in\n     *\/\n    private void logout() {\n        if (myName == null) {\n            System.out.printf(&quot;No user is logged in.n&quot;);\n            return;\n        }\n        leaveRoom(null);\n        System.out.printf(&quot;%s is logged out.n&quot;, myName);\n        myName = null;\n        birthday = null;\n    }\n\n    private PrintStream register() throws NoSuchFieldException, IllegalAccessException {\n        String username;\n        String password, password2;\n\n        \/\/ Check if already logged in\n        if (myName != null)\n            return System.out.printf(&quot;You are already logged in as %s. To register a new user log out first.n&quot;, myName);\n        \n        \/\/ Create a registration query\n        LoginQuery query = createQuery(LoginQuery.class);\n        while(true) {\n            \/\/ Get user name\n            System.out.printf(&quot;(type Enter to exit login)n&quot;);\n            username = readConsoleLine(&quot;User name:&quot;);\n            if (username.isBlank())\n                return exitRegistration();\n            do {\n                \/\/ Get password\n                password = readConsoleLine(&quot;Password:&quot;);\n                if (password.isBlank())\n                    return exitRegistration();\n\n                \/\/ Repeat the password\n                password2 = readConsoleLine(&quot;Repeat password:&quot;);\n                if (password2.isBlank())\n                    return exitRegistration();\n                \n                \/\/ Check if passwords are equal\n                if (password.equals(password2))\n                    break;\n                System.out.println(&quot;Passwords do not match.&quot;);\n            } while (true);\n            \n            \/\/ Get and parse birth date\n            do {\n                String s = readConsoleLine(&quot;Date of birth (yyyy\/mm\/dd):&quot;);\n                if (s.isBlank())\n                    return exitRegistration();\n                try {\n                    birthday = ZDate.parseTime(s, &quot;yyyy\/MM\/dd&quot;, null);\n                } catch (ParseException ex) {\n                    birthday = null;\n                    System.out.printf(&quot;Invalid date %s. Please try again.n&quot;, s);\n                }\n            } while (birthday == null);\n\n            \/\/ Post the registration query\n            query.setNewUser(true);\n            query.setName(username);\n            query.setPassword(password);\n            query.setBirthday(birthday);\n            query.post(5 * ZDate.SECOND);\n\n            \/\/ If the query did not expire check login results, if it did, repeat.\n            if (query.waitForReply()) {\n                switch (query.getResult()) {\n                case OK:\n                    myName = username;\n                    return System.out.printf(&quot;Registered and logged in successfully as %s.n&quot;, myName);\n                case EXISTS:\n                    System.out.printf(&quot;User %s already exista. Please try again with another name.n&quot;, username);\n                    break;\n                }\n            } else\n                return unavailableUserManager();\n        }\n    }\n    \n    \/**\n     * Print a message when login existed by typing a blank Enter\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream exitLogin() {\n        return System.out.printf(&quot;Login exitedn&quot;);\n    }\n    \n    \/**\n     * Print a message when registration existed by typing a blank Enter\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream exitRegistration() {\n        return System.out.printf(&quot;Registration exitedn&quot;);\n    }\n    \n    \/**\n     * Print a message when user management is not available\n     * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding.\n     *\/\n    private PrintStream unavailableUserManager() {\n        return System.out.printf(&quot;No user management entity is available to handle this requestn&quot;);\n    }\n    \n    \/**\n     * Join a room after checking that the room exists, its adult setting matches user configuration,\n     * and the user is not already there. If the user is in another room leave that room first.\n     * @param room  the new room name\n     * @throws NoSuchFieldException     If any of the created data objects does not define ObjectCode static field.\n     * @throws IllegalAccessException   If ObjectCode is not public.\n     *\/\n    private synchronized void joinRoom(String room) throws NoSuchFieldException, IllegalAccessException {\n        \/\/ User must be logged in\n        if (myName == null) {\n            System.out.printf(&quot;You must log in before joining a room.n&quot;);\n            return;\n        }\n        \n        \/\/ Check whether room exists and the user is not already in the room\n        ChatRoomConsume chatRoom = getRootObject().getChild(ChatRoomConsume.class, room);\n        if (chatRoom == null) {\n            System.out.printf(&quot;Room %s does not existn&quot;, room);\n            return;\n        }\n\n        if (chatter != null &amp;&amp; room.equalsIgnoreCase(chatter.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 = ZUtilities.parseInt(arg.substring(0, arg.length() - 1));\n            if (n &lt;= 0)\n                return false;\n            int unit;\n            switch (arg.substring(arg.length() - 1)) {\n            case &quot;h&quot;:\n                unit = ZDate.HOUR;\n                break;\n            case &quot;d&quot;:\n                unit = ZDate.DAY;\n                break;\n            default:\n                return false;\n            }\n            since = ZDate.now().add(-n * unit);\n        }\n        \n        \/\/ create a query and post it\n        ChatHistoryQuery query = createQuery(ChatHistoryQuery.class);\n        query.setUsername(myName);\n        query.setSince(since);\n        query.post(5 * ZDate.SECOND);\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>chatter<\/code> field\nis now defined as <code>ChatterConsume<\/code>\n(line 37). We will see in a second why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ChatApp<\/code> is added\nto the produced object list (line 105). This is needed for the routing\nprocedure of peer applications. For this reason it is also added to the\nconsumed object list (line 121). The class is also registered in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#populateObjectFactory(java.util.List)\">populateObjectFactory()<\/a> (line 137).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ChatRoom<\/code> is now\nthe grandparent of <code>Chatter<\/code> rather\nthan its immediate parent and we have to consider this change when we extract\nthe room name from a <code>Chatter<\/code> object.\nFor that purpose we added the <code>getRoomName()<\/code>\nmethod to <code>ChatterConsume<\/code>. You can see\nits use in lines 412, 465 and 473. Also in <code>findUser()<\/code>\n(line 484), which is supposed to return an application UUID, we now return the\nobject ID of the parent of the found <code>Chatter<\/code>\nobject rather than its own ID.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When the user joins a room, we need to first create a <code>ChatApp<\/code> object under the desired <code>ChatRoom<\/code> object and then create a <code>Chatter<\/code> object under it. This is done in\nlines 424 \u2013 428.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Similarly, when the user leaves the room, we first remove\nthe <code>Chatter<\/code> object and commit the\naction so that peer users are notified, then remove the <code>ChatApp<\/code> object and commit it so that peer applications know not to\nroute room messages to this application. This is done in lines 443 \u2013 452.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We removed <code>isMemberOfMyRoom()<\/code>\nall together because we now do it in <code>ChatMessageImp<\/code>\nas we will see in a moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Some implementation data objects are also modified. Let\u2019s\nsee <code>ChatterConsume<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.chat;\n\nimport org.spiderwiz.tutorial.objectLib.Chatter;\n\n\/**\n * Consumer implementation of Chatter data object. Notify when a chatter enters or leaves my room.\n *\/\npublic class ChatterConsume extends Chatter {\n\n    \/**\n     * Notify when a chatter enters my room.\n     *\/\n    @Override\n    protected void onNew() {\n        if (!isStealth() &amp;&amp; ChatMain.getInstance().isSameRoom(getRoomName()) &amp;&amp; getName() != null)\n            System.out.printf(&quot;%s entered the roomn&quot;, getName());\n    }\n\n    \/**\n     * Notify when a chatter leaves my room. If the chatter is myself leave the room gracefully.\n     * @return true to confirm the removal.\n     *\/\n    @Override\n    protected boolean onRemoval() {\n        if (ChatMain.getInstance().getAppUUID().toString().equals(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}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">As mentioned above, we added the <code>getRoomName()<\/code> method (line 34) that returns the object ID of the <strong>grandparent<\/strong> of this object. This is used in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onNew()\">onNew()<\/a> (line 15) and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRemoval()\">onRemoval()<\/a> (line 24).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also in <code>onRemoval()<\/code>\nwe compare the application UUID against the object ID of the parent of this\nobject rather than its own ID.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And finally <code>ChatMessageImp<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.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;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\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.\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            String name = ((Chatter)getParent()).getName();\n            if (name == null)\n                name = getParent().getObjectID();\n            System.out.printf(&quot;%1$s: %2$sn&quot;, name, 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.\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; isMemberOfMyRoom(appUUID);\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    \n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onEvent()\">onEvent()<\/a> (line 20) is slightly changed to fetch\nthe room name from the great-grandparent of the object rather than from its\ngrandparent. We also check if the parent\u2019s <code>name<\/code>\nproperty is <code>null<\/code> before printing the\nmessage, because when data is imported from the foreign system we cannot trust\nthat we get a <code>Chatter<\/code> object before\nreceiving this <code>ChatMessage<\/code> object. If\nit is <code>null<\/code>, we take the user name\nfrom the object ID of the parent (this is always available since, when\nreceiving a <code>ChatMessage<\/code> object whose\nparent is not recognized, the framework creates an empty <code>Chatter<\/code> object and uses it as the parent of the received object).\nNote that if the <code>name<\/code> property is not\n<code>null<\/code> then we use it rather than the\nobject ID because the latter is case insensitive while we prefer to display a\ncase sensitive name.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As mentioned above we have moved <code>isMemberOfMyRoom()<\/code> from <code>ChatMain<\/code>\nto this class (line 54). The method is used in <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> (line 42). The method checks\nif the great-grandparent of the current object, which is a <code>ChatRoom<\/code> object, has a child whose object ID equals the <code>appUUID<\/code> parameter. Recall that <code>ChatRoom<\/code> children are all the <code>ChatApp<\/code> objects that are in the room.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We are done with all the necessary modifications to the chat\napplication. Let\u2019s build the <em>Chat Room\nBridge<\/em>. Here is <code>BridgeMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.bridge;\n\nimport java.util.List;\nimport org.spiderwiz.core.DataObject;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.tutorial.objectLib.ConsoleMain;\n\n\/**\n * Provides the entry point of the application. Initializes and executes the Spiderwiz framework.\n *\/\npublic class BridgeMain extends ConsoleMain {\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String CONF_FILENAME = &quot;bridge.conf&quot;;\n    private static final String APP_NAME = &quot;Chat Room Bridge&quot;;\n    private static final String APP_VERSION = &quot;Z1.01&quot;;  \/\/ Version Z1.01: Initial version\n\n    \/**\n     * Class constructor with constant parameters. Call super constructor and create the user map.\n     *\/\n    public BridgeMain() {\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 BridgeMain().init();\n    }\n\n    \/**\n     * @return the list of produced objects\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{\n            Chatter.ObjectCode,\n            ChatMessage.ObjectCode,\n            ObjectCodes.RawExport       \/\/ predefined code for exporting raw data to a remote service\n        };\n    }\n\n    \/**\n     * @return the list of consumed objects\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{\n            Chatter.ObjectCode,\n            ChatMessage.ObjectCode,\n            ObjectCodes.RawImport       \/\/ predefined code for importing raw data from a remote service\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(ChatRoom.class);\n        factoryList.add(ChatApp.class);\n        factoryList.add(ChatterBridge.class);\n        factoryList.add(ChatMessageBridge.class);\n    }\n\n    \/**\n     * Process an input line - nothing to process in our case.\n     * @param line      The input line\n     * @return true\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">The goal of the bridge is to import foreign data, convert it\nto Spiderwiz native Data Objects and distribute them. So the application\nproduces (line 40) <code>Chatter<\/code> and <code>ChatMessage<\/code> objects. (You can see that\nit also produces <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.ObjectCodes.html#RawExport\">ObjectCodes.RawExport<\/a>. We will show its use later\nbut for now you can safely ignore it).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Similarly, the bridge exports native Data Objects to the\nforeign system, so it consumes (line 52) the same objects (plus <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.ObjectCodes.html#RawImport\">ObjectCodes.RawImport<\/a> that will be explained\nlater and may be ignored now).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/Main.html#populateObjectFactory(java.util.List)\">populateObjectFactory()<\/a> (line 65) registers <code>ChatRoom<\/code>, <code>ChatApp<\/code> and the implementation classes <code>ChatterBridge<\/code> and <code>ChatMessageBridge<\/code>. Nothing to do in <code>processConsoleLine()<\/code> (line 79).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The interesting work is done in the implementation classes.\nHere is <code>ChatterBridge<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.bridge;\n\nimport org.spiderwiz.core.ImportHandler;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.zutils.ZDate;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Extend Chatter to implement import\/export from\/to an external chat system.\n * Data from the external system is received as text lines, each in the form of:\n * &lt;room name&gt;,&lt;chatter name&gt;,&lt;message&gt;\n * The line can report that a user joined a room with:\n * !join,&lt;room name&gt;,&lt;chatter name&gt;\n * When a user leaves a room the following line reports it:\n * !leave,&lt;room name&gt;&lt;chatter name&gt;\n *\/\npublic class ChatterBridge extends Chatter {\n\n    \/**\n     * Parse import lines and handle them if they are either !join or !leave command\n     * @param data          the imported data line.\n     * @param channel       the handler of the channel the data is imported from. Shall be &quot;bridge&quot;.\n     * @param ts            the timestamp attached to the data by the channel handler.\n     * @return the key hierarchy of the imported object - room name then application UUID then user name, or null\n     * if this is not a !join command\n     * @throws Exception\n     *\/\n    @Override\n    protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception {\n        \/\/ check if the import handler name is &quot;bridge&quot;\n        if (!&quot;bridge&quot;.equalsIgnoreCase(channel.getName()))\n            return null;\n        \n        \/\/ parse the line\n        String elements[] = data.toString().split(&quot;,&quot;, 3);\n        if (elements.length &lt; 3)\n            return null;\n        switch(elements[0]) {\n        case &quot;!leave&quot;:\n            \/\/ Import the removal then proceed as if joined.\n            if (remove() == null)\n                return null;\n        case &quot;!join&quot;:\n            \/\/ Set user name and return the key list of the joining (or leaving) user\n            setName(elements[2]);\n            return new String[] {elements[1], getMyUUID(), getName()};\n        }\n        \n        \/\/ In all other cases return null\n        return null;\n    }\n\n    \/**\n     * Export a !join or !leave command\n     * @param channel   the handler of the channel the object will be exported to. Shall be &quot;bridge&quot;.\n     * @param newID     null if this object is active, empty string if it has been removed, non-empty string if it has been renamed.\n     * @return          the serialized object to export\n     *\/\n    @Override\n    protected String exportObject(ImportHandler channel, String newID) {\n        \/\/ Do not export my own object\n        if (BridgeMain.getInstance().getAppUUID().equals(getOriginUUID()))\n            return null;\n        \n        \/\/ check if the import handler name is &quot;bridge&quot;\n        if (!&quot;bridge&quot;.equalsIgnoreCase(channel.getName()))\n            return null;\n        \n        \/\/ determine if we need to export a &quot;!join&quot; or a &quot;!leave&quot; command, then generate the command\n        String command = newID == null ? &quot;!join&quot; : &quot;!leave&quot;;\n        return ZUtilities.concatAll(&quot;,&quot;, command, getRoomName(), getName());\n    }\n\n    \/**\n     * @return true to make Chatter as urgent as ChatMessage, otherwise object imported from a sequential file might be delivered\n     * in the wrong order.\n     *\/\n    @Override\n    protected boolean isUrgent() {\n        return true;\n    }\n\n    \/**\n     * When a new chatter in a room is encountered, export it\n     *\/\n    @Override\n    protected void onNew() {\n        commit(&quot;&quot;);\n    }\n\n    \/**\n     * When a chatter leaves a room, export the command\n     * @return true to confirm the removal.\n     *\/\n    @Override\n    protected boolean onRemoval() {\n        \/\/ Done exactly as oneNew(). exportObject() detects whether this is a join or a leave case.\n        commit(&quot;&quot;);\n        return true;\n    }\n    \n    \/**\n     * @return the current application UUID as a string\n     *\/\n    private String getMyUUID() {\n        return BridgeMain.getInstance().getAppUUID().toString();\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}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">This is where <code>Chatter<\/code> objects are imported and exported. Import is done in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#importObject(java.lang.Object,org.spiderwiz.core.ImportHandler,org.spiderwiz.zutils.ZDate)\">importObject()<\/a> (line 29). Its parameters are the imported raw <code>data<\/code>, the <code>channel<\/code> through which the data is received and a time stamp. We first check if the channel name is <code>\"bridge\"<\/code>. This is necessary because in theory data can be imported from multiple sources in various formats, and in order to interpret it we need to identify the source.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We then parse the line. Lines that are relevant to <code>Chatter<\/code> objects start with either <code>\"!join\"<\/code> or <code>\"!leave\"<\/code>. The first is received when a chatter joins a room. In both cases we set the <code>name<\/code> property of the current object and return a string array that contains the object key list starting from the root \u2013 room name that identifies the grandparent <code>ChatRoom<\/code> object, the current application UUID that identifies the parent <code>ChatApp<\/code> object and the user name that identifies the current object. The difference is that In the <code>\u201c!leave\u201d<\/code> case we first call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#remove()\">remove()<\/a> on the object to mark it for deletion. That\u2019s it!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Export is done in <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#exportObject(org.spiderwiz.core.ImportHandler,java.lang.String)\">exportObject()<\/a> (line 60). First we verify that we do not bounce an imported object by comparing the application UUID to the object\u2019s origin UUID. We then make sure that the export channel is <code>\"bridge\"<\/code>. Finally we construct either a <code>\"!join\"<\/code> or <code>\"!leave\"<\/code> command depending on the value of the <code>newID<\/code> parameter. We return the constructed string and we are done.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reason we override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#isUrgent()\">isUrgent()<\/a> to return <code>true<\/code> (line 79) is that, if you still remember, <code>ChatMassage.isUrgent()<\/code> returns <code>true<\/code>, and if both classes do not return the same value then we cannot guarantee the order by which the imported objects are distributed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To activate export we need to override <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onNew()\">onNew()<\/a> (line 87) and <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onRemoval()\">onRemoval()<\/a> (line 96). In both cases we call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#commit(java.lang.String)\">commit(&#8220;&#8221;)<\/a>, which is what activates export (and native distribution). We use the empty string parameter because if we used the non-parameterized <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#commit()\">commit()<\/a> then the object would be bounced to the current application, and that would trigger an endless (until heap memory is exploited) recursive call to <code>onNew()<\/code> or <code>onRemoval()<\/code>. The empty string means \u201cdo not send it to any consumer but still export it\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other implementation class is <code>ChatMessageBridge<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.bridge;\n\nimport java.util.Map;\nimport java.util.UUID;\nimport org.spiderwiz.core.ImportHandler;\nimport org.spiderwiz.tutorial.objectLib.ChatApp;\nimport org.spiderwiz.tutorial.objectLib.ChatMessage;\nimport org.spiderwiz.tutorial.objectLib.ChatRoom;\nimport org.spiderwiz.tutorial.objectLib.Chatter;\nimport org.spiderwiz.zutils.ZDate;\nimport org.spiderwiz.zutils.ZUtilities;\n\n\/**\n * Implementation class of ChatMessagee for Chat Room Bridge.\n * Data from and to the external system is delivered as text lines, each in the form of:\n * &lt;room name&gt;,&lt;chatter name&gt;,&lt;message&gt;\n*\/\npublic class ChatMessageBridge extends ChatMessage {\n\n    \/**\n     * If this is not a bounced message that was imported by this application then commit it to itself in order to export it.\n     * @return true;\n     *\/\n    @Override\n    protected boolean onEvent() {\n        commit(&quot;&quot;);\n        return true;\n    }\n\n    \/**\n     * Parse import lines of the form &lt;room name&gt;,&lt;chatter name&gt;,&lt;message&gt; and use the elements to feed\n     * ChatMessage objects to the system.\n     * @param data          the imported data string.\n     * @param channel       the handler of the channel the data is imported from.\n     * @param ts            the timestamp attached to the data by the channel handler.\n     * @return the key hierarchy of the imported object - room name then application UUID\n     * @throws Exception\n     *\/\n    @Override\n    protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception {\n        \/\/ check if the import handler name is &quot;bridge&quot;\n        if (!&quot;bridge&quot;.equalsIgnoreCase(channel.getName()))\n            return null;\n        \n        \/\/ Parse the line and process it\n        String elements[] = data.toString().split(&quot;,&quot;, 3);\n        if (elements.length &lt; 3)\n            return null;\n        switch(elements[0]) {\n        case &quot;!join&quot;:\n        case &quot;!leave&quot;:\n            return null;\n        }\n        \n        setMessage(elements[2]);\n        setTime(ts);\n        \/\/ Return the key sequence\n        return new String[] {elements[0], BridgeMain.getInstance().getAppUUID().toString(), elements[1]};\n    }\n\n    \/**\n     * Serialize the object for exporting.\n     * @param channel   the handler of the channel the object will be exported to.\n     * @param newID     null if this object is active, empty string if it has been removed, non-empty string if it has been renamed.\n     *                  In this case it is always null because ChatMessage is disposable.\n     * @return          the serialized object to export in the form &lt;room name&gt;,&lt;chatter name&gt;,&lt;message&gt;.\n     *                  If the object is imported we don't want to bounce it so we return null;\n     *\/\n    @Override\n    protected String exportObject(ImportHandler channel, String newID) {\n        \/\/ Don't bounce imported objects\n        if (BridgeMain.getInstance().getAppUUID().equals(getOriginUUID()))\n            return null;\n        \n        \/\/ check if the import handler name is &quot;bridge&quot;\n        if (!&quot;bridge&quot;.equalsIgnoreCase(channel.getName()))\n            return null;\n\n        \/\/ serialize\n        String room = getParent().getParent().getParent().getObjectID();\n        String chatter = ((Chatter)getParent()).getName();\n        return ZUtilities.concatAll(&quot;,&quot;, room, chatter, getMessage());\n    }\n\n    \/**\n     * Make sure the object is delivered to the right destinations, i.e. to chatters in the same room or to other bridge applications.\n     * @param appUUID       Application UUID of the destination\n     * @param appName       Application name of the destination. Not relevant here.\n     * @param userID        User ID used for establishing communication. N\/A.\n     * @param remoteAddress Remote address of the destination application. N\/A.\n     * @param appParams     application parameter map of the destination. If it includes &quot;role&quot; -&gt; &quot;bridge&quot; mapping\n     *                      then deliver all messages with no further filtering.\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            \/\/ Check if the destination is for the same room\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 ChatApp does not contain ObjectCode static field or the field is not public.\n            \/\/ In this case, send an exception message.\n            BridgeMain.getInstance().sendExceptionMail(ex, &quot;Cannot instantiate ChatApp class&quot;, null, false);\n            return false;\n        }\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Here we do pretty much the same thing. In <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#onEvent()\">onEvent()<\/a> (line 25) we call <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#commit(java.lang.String)\">commit(\u201c\u201d)<\/a> to activate export (avoiding recursive\ncommits). In <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#importObject(java.lang.Object,org.spiderwiz.core.ImportHandler,org.spiderwiz.zutils.ZDate)\">importObject()<\/a> (line 40)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; we verify that the import channel is <code>\u201cbridge\u201d<\/code>, parse the imported data (that is supposed to be a text\nstring), and if it is a chat message we store the values in the current object\nand return its hierarchy key as a string array \u2013 [room name, current\napplication UUID, user name]. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/DataObject.html#exportObject(org.spiderwiz.core.ImportHandler,java.lang.String)\">exportObject()<\/a> (line 70) we verify that we do not\nbounce an imported object and that the export channel is <code>\u201cbridge\u201d<\/code>, then we construct the export message from the object\nproperties and return it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <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> method (line 96) is called\nduring the distribution process of the object that was imported by <code>importObject()<\/code>. We override it to\nrestrict object routing to applications whose users are in the same room as the\nobject, just like the chat applications do.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This completes our coding mission. And where is the actual\ncommunication with the foreign system? Nice guess! In the configuration file of\ncourse. Here it is:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/Bridge\/Logs\n[producer-1]ip=localhost;port=10001\n[import-1]infile=\/tests\/Remote\/ImportIn.txt;outfile=\/tests\/Remote\/ImportOut.txt;name=bridge<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">You can see one new property \u2013 <code>import-1<\/code>. In this case we use text files for input and output, but\nyou can also use <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/config.html#ImportConnection\">other configurations.<\/a> You can also <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-17\/\">write\nyour own import handler plugin<\/a> to extend the default one or to\nsupport other types of foreign data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note that this property includes the parameter <code>name=bridge<\/code>. This gives the input\nchannel the <code>\u201cbridge\u201d<\/code> name that is\nchecked a few times in the code described above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One more thing for your attention \u2013 when you run the\napplication with this test scenario you will see that a new folder is created\nunder the configured log folder named <code>imports<\/code>.\nThe folder will contain a sub folder named <code>bridge<\/code>.\nSee <a href=\"http:\/\/spiderwiz.org\/apidocs\/org\/spiderwiz\/core\/doc-files\/logging.html\">Spiderwiz Logging System<\/a> for more details.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Import-Export\ndelegation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We still owe you an explanation about the <code>ObjectCodes.RawExport<\/code> and <code>ObjectCodes.RawImport<\/code> constants that we\nuse in <code>BridgeMain.getProducedObjects()<\/code>\nand <code>BridgeMain.getConsumedObjects()<\/code>\nrespectively. These are not needed for the example we implemented above, but\nmight be very useful in the following scenario:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When talking about interfacing with an existing system, it\nhappens often that these are production systems that programmers have limited\naccess to. The \u201cbridge\u201d that we built in this lesson requires programming for\nevery specific Data Object that is imported from or exported to the foreign\nsystem. If the access to the import channels referred to by the bridge is\nrestricted to modules running on the same production system then the bridge\nmust be collocated on the same system, and then every change to it or the\naddition of more bridges requires the unwelcome programmer access.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What you would prefer to do in this case is to have a\none-time installation of a \u201ccatch all\u201d bridge. This is a Spiderwiz application\nthat produces the entire raw data as received from the foreign system\nencapsulated in <code>RawImport<\/code> data\nobjects. The consumers are the applications that run on a development system,\nwhich parse the data and produce specific data objects. Similarly, the\ndevelopment applications encapsulate exported data in <code>RawExport<\/code> data objects, which are received by the production bridge\nand delivered as raw data to the foreign system. This is exactly what the\nbuilt-in data objects identified by the said constants do.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In fact they do much more because they completely free the\ndevelopers from worrying about the location of the foreign system and whether\nit is accessed directly or through a bridge. In the example demonstrated in\nthis lesson we can have the <code>import-n<\/code>\nconfiguration property as above, in which case the property define direct\naccess parameters to the foreign system, or we can omit this property all\ntogether and instead install somewhere a \u201ccatch all\u201d bridge that connects to\nthe foreign system and exchanges the necessary objects with the <em>Chat Room Bridge<\/em> described here. The\nlatter works without any change whether configured like in the first or the\nsecond way.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To have the whole picture we will demonstrate the\nimplementation of the remote bridge. Here is <code>RemoteMain<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>package org.spiderwiz.tutorial.lesson11.remote;\n\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 RemoteMain extends ConsoleMain{\n    private static final String ROOT_DIRECTORY = &quot;&quot;;\n    private static final String CONFIG_FILE_NAME = &quot;remote.conf&quot;;\n    private static final String APP_NAME = &quot;Remote Import\/Export&quot;;\n    private static final String APP_VERSION = &quot;Z1.01&quot;;  \/\/ Version Z1.01: Initial version\n\n    public RemoteMain() {\n        super(ROOT_DIRECTORY, CONFIG_FILE_NAME, 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 RemoteMain().init();\n    }\n\n    \/**\n     * @return a predefined code for importing raw data into a remote service\n     *\/\n    @Override\n    protected String[] getProducedObjects() {\n        return new String[]{ObjectCodes.RawImport};\n    }\n\n    \/**\n     * @return a predefined code for exporting raw data from a remote service\n     *\/\n    @Override\n    protected String[] getConsumedObjects() {\n        return new String[]{ObjectCodes.RawExport};\n    }\n\n    \/**\n     * Process an input line - nothing to process in our case.\n     * @param line      The input line\n     * @return true\n     *\/\n    @Override\n    protected boolean processConsoleLine(String line) {\n        return true;\n    }\n}<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">As you see, this is a barebones <code>ConsoleMain<\/code> extension, except that it produces <code>ObjectCodes.RawImport<\/code> (line 31) and consumes <code>ObjectCodes.RawExport<\/code> (line 29). Nothing else is required since\neverything is already built into the framework.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The configuration file is:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/Remote\/Logs\n[producer-1]ip=localhost;port=10001\n[import-1]infile=\/tests\/Remote\/ImportIn.txt;outfile=\/tests\/Remote\/ImportOut.txt;name=bridge<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">and if we do import-export through this application then we\ncan omit the import definition of <code>bridge.conf<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/Bridge\/Logs\n[producer-1]ip=localhost;port=10001<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">The magic of\nSpiderAdmin<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you run the examples of this lesson you may discover that\nthey do not work as you might except. The problem is that our tests use local\nfiles for accessing \u201cforeign data\u201d, and the input file is opened and read\nduring application initialization. Most probably, when the file is read,\nhandshake procedures on other communication channels have not yet been\ncompleted and therefore the processed file data cannot be delivered to other\napplications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We want to activate file processing only after all other communication channels are fully established. This is done easily with the \u201chot reconfiguration\u201d feature of <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-14\/\">SpiderAdmin<\/a>. Here is how it works:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Initially when we run <em>Chat\nRoom Bridge<\/em> we comment out the definition of the import channel, like this:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre>[log folder]\/tests\/Bridge\/Logs\n[producer-1]ip=localhost;port=10001\n[-import-1]infile=\/tests\/Remote\/ImportIn.txt;outfile=\/tests\/Remote\/ImportOut.txt;name=bridge<\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">We run all the services that constitute the chat system,\nassuring that <em>My Hub<\/em> is connected to <em>SpiderAdmin<\/em> as in <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-3\/\">Lesson 3<\/a>.\nWhen we surf to <a href=\"http:\/\/spideradmin.com\">SpiderAdmin<\/a>\nand log into the service, we should see something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"520\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-1-1024x520.png\" alt=\"\" class=\"wp-image-1030\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-1-1024x520.png 1024w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-1-300x152.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-1-768x390.png 768w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-1.png 1275w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Look for <em>Chat Room\nBridge <\/em>In the <strong>Applications<\/strong> table\nand click it. You should see this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"434\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-2-1024x434.png\" alt=\"\" class=\"wp-image-1032\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-2-1024x434.png 1024w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-2-300x127.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-2-768x325.png 768w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-2.png 1277w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You see a row of buttons under the page title. Click <strong>Update Configuration<\/strong> and you will see\nthis:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"429\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-3-1024x429.png\" alt=\"\" class=\"wp-image-1033\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-3-1024x429.png 1024w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-3-300x126.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-3-768x322.png 768w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-3.png 1277w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Remove the commenting minus sign in front of the <code>import-1<\/code> property, like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"774\" height=\"203\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-4.png\" alt=\"\" class=\"wp-image-1034\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-4.png 774w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-4-300x79.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-4-768x201.png 768w\" sizes=\"auto, (max-width: 774px) 100vw, 774px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Then click the <span class=\"boldgreen\">\u2713<\/span> symbol at the top right of the pop-up window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <em>Chat Room Bridge<\/em>\nconsole now shows<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>Connection to ImportIn.txt succeeded<\/code><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">and the imported data from that file shows up in the console\nof the chat applications that have joined the relevant rooms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wait a little while until SpiderAdmin page refreshes or\nrefresh it manually and you will see that the \u201cbridge\u201d import channel appeared\nin the <strong>Import Channels<\/strong> table:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"593\" src=\"http:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-5-1024x593.png\" alt=\"\" class=\"wp-image-1035\" srcset=\"https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-5-1024x593.png 1024w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-5-300x174.png 300w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-5-768x444.png 768w, https:\/\/spiderwiz.org\/project\/wp-content\/uploads\/2020\/08\/lesson-11-5.png 1277w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">We are very close to having the full picture of the\nSpiderwiz Programming Model. We will conclude that part of the tutorial in the <a href=\"http:\/\/spiderwiz.org\/project\/tutorial\/lesson-12\/\">next\nlesson<\/a> when we discuss Lossless Data Objects.<strong><\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn a tricky way to easily bridge between the state-of-the-art Spiderwiz-based applications and legacy frameworks.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":145,"menu_order":11,"comment_status":"closed","ping_status":"closed","template":"tutorial-child.php","meta":{"footnotes":""},"class_list":["post-986","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/986","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=986"}],"version-history":[{"count":25,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/986\/revisions"}],"predecessor-version":[{"id":1424,"href":"https:\/\/spiderwiz.org\/project\/wp-json\/wp\/v2\/pages\/986\/revisions\/1424"}],"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=986"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}