After ten lessons you are probably excited about Spiderwiz as much as we are, but the framework has one drawback – not everybody uses it yet. When you develop with Spiderwiz, it is quite probable that you would need to interface with legacy systems that use different ways of data delivery. Spiderwiz Import-Export mechanism eases this process substantially.
The concept follows the framework paradigm that everything that happens to a Data Object is encapsulated within its class code. Therefore data objects import themselves and export themselves. In this lesson we will see how it works.
To demonstrate the topic we will use – guess what – our chat system of the previous lessons. Assuming there is another chat system, implemented by other means, that accesses the same chat rooms. We want to bridge the two systems so that the activity of chatters that use one will be visible to chatters that use the other.
To simplify the demonstration, we assume that information that passes between the two systems, let’s say over a TCP/IP socket, is in the form of text lines, each contains a message in one of the following formats:
- !join,<room name>,<chatter name>
- !leave,<room name><chatter name>
- <room name>,<chatter name>,<message>
The first is sent when a user joins a room, the second when the user leaves it and the third when a user posts a message in a room.
We are going to add a new service to our mesh called “Chat Room Bridge”. Its role is to
connect to the foreign system, read lines in the said formats, parse them and
feed them to our system as native Chatter
and ChatMessage
objects. On the other
hand the service listens to our chat rooms, converts Chatter
and ChatMessage
objects to text lines of the above formats and transmits them to the foreign
system.
Before starting, we need to tackle a small architectural problem. In the previous lessons we had one chat application used by one user. This let us identifying the user by the application UUID, which we used as a trick to easily route messages to the proper destinations. This trick will not work here, because we are going to have one application, the bridge, that represents all the users across it.
The solution is to make a slight change in the hierarchy of
our Data Object Tree. We will insert a new data object class, ChatApp
, as a child of ChatRoom
and a parent of Chatter
. ChatApp
has no properties and is identified by the application
UUID. Its children of type Chatter
are identified by the user name. As there can be multiple Chatter
children to a single ChatApp
object, the problem is solved elegantly.
With this change, ChatRoom
remains as it is. The new ChatApp
class is just a barebones Data Object, defined as a non-disposable child of ChatRoom
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.core.DataObject; /** * A DataObject class that represents a chat application within a specific chat room. It is the parent of all the chatters that * are in that application and joined the room that is the parent of this object. */ public class ChatApp extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHTAPP"; @Override protected String getParentCode() { return ChatRoom.ObjectCode; } @Override protected boolean isDisposable() { return false; } }
Here is the modified Chatter
class:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; import org.spiderwiz.zutils.ZDate; /** * A data object that represents a chat user using a specific chat application in a specific chat room */ public class Chatter extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHTTR"; @WizField private String name; // Contains the name of the chatter @WizField private ZDate birthday; // Contains the chatter birth date @WizField private boolean stealth = false; // True if chatters shall not be notified when this user joins or leave a room public String getName() { return name; } public void setName(String name) { this.name = name; } public ZDate getBirthday() { return birthday; } public void setBirthday(ZDate birthday) { this.birthday = birthday; } public boolean isStealth() { return stealth; } public void setStealth(boolean stealth) { this.stealth = stealth; } /** * @return ChatRoom object code */ @Override protected String getParentCode() { return ChatApp.ObjectCode; } /** * @return false because objects of this class are persistent */ @Override protected boolean isDisposable() { return false; } /** * The object ID, which is the user name, is case insensitive. * @return */ @Override protected boolean isCaseSensitive() { return false; } }
The class is now defined (line 48) as a child of ChatApp
. We also define it as case
insensitive (line 65) since the object ID is now the user name that we do not
want to bother with its case.
There is no change in ChatMessage
and other library classes.
The new structure requires few modifications to the chat
application. Here is ChatMain
:
package org.spiderwiz.tutorial.lesson11.chat; import java.io.PrintStream; import java.text.ParseException; import java.util.Collection; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ActiveUserQuery; import org.spiderwiz.tutorial.objectLib.Chat; import org.spiderwiz.tutorial.objectLib.ChatApp; import org.spiderwiz.tutorial.objectLib.ChatHistoryQuery; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.tutorial.objectLib.ConsoleMain; import org.spiderwiz.tutorial.objectLib.LoginQuery; import org.spiderwiz.zutils.ZDate; import org.spiderwiz.zutils.ZUtilities; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class ChatMain extends ConsoleMain{ private static final String ROOT_DIRECTORY = ""; private static final String APP_NAME = "Chat Room Client"; private static final String APP_VERSION = "Z11.01"; // Version Z11.01: Lesson 11 modifications private static final String JOIN = "!join"; // join a chat room command private static final String LEAVE = "!leave"; // leave current chat room command private static final String LOGIN = "!login"; // start login procedure private static final String LOGOUT = "!logout"; // log the user out private static final String REGISTER = "!register"; // start registration procedure private static final String HISTORY = "!history"; // print my chat history private String myName = null; // The user login name private ZDate birthday = null; // User birth date. Get it when logging in private ChatterConsume chatter = null; // A Chatter object representing the user chatting in a specific room private ChatMessage message = null; // A ChatMessage object for committing chat messages private Chat privateMessage = null; // A Chat object for committing private messages /** * Class constructor with constant parameters. * @param confFileName configuration file name, provided as a command argument */ public ChatMain(String confFileName) { super(ROOT_DIRECTORY, confFileName, APP_NAME, APP_VERSION); } /** * @return the class instance as ChatMain type. */ public static ChatMain getInstance() { return (ChatMain)ConsoleMain.getInstance(); } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. The first argument is used as the configuration file name. */ /** * @param args the command line arguments */ public static void main(String[] args) { // Don't do anything if there is no configuration file name if (args.length == 0) { System.out.println("Configuration file has not been defined"); return; } new ChatMain(args[0]).init(); } /** * Print user instructions to the console. * @return true */ @Override protected boolean preStart() { System.out.printf( "Welcome to the dynamic chat system.n" + "To log in type !login.n" + "To log out type !logout.n" + "To register type !register.n" + "To join a chat room or change your current chat room, type "!join <room name>".n" + "To leave your chat room, type "!leave".n" + "To send a message in the current chat room just type the message " + "(a message cannot start with either '>' or '!').n" + "To send a private message type '>' followed by name of the user you want to send " + "the private message to, followed by ':' for by the message.n" + "To print the history of your own messages, type "!history <hours>h" or "!history <days>d"n" + "(for instance "!history 3h" to see messages since 3 hours ago and "!history 1d" to see messages since 1 day" + " ago).n" + "or type just "!history" to see all your messages.n" + "To exit type 'exit'.n" ); return true; } /** * @return the list of produced objects - Chatter, Chat, ChatMessage and LoginQuery */ @Override protected String[] getProducedObjects() { return new String[]{ ChatApp.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode, LoginQuery.ObjectCode, ChatHistoryQuery.ObjectCode }; } /** * @return the list of consumed objects - ChatRoom, Chatter, Chat, ChatMessage and ActiveUserQuery */ @Override protected String[] getConsumedObjects() { return new String[]{ ChatRoom.ObjectCode, ChatApp.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode, ActiveUserQuery.ObjectCode }; } /** * Add implementation classes to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(ChatRoomConsume.class); factoryList.add(ChatApp.class); factoryList.add(ChatterConsume.class); factoryList.add(ChatConsume.class); factoryList.add(ChatMessageImp.class); factoryList.add(LoginQuery.class); factoryList.add(ActiveUserQueryReply.class); factoryList.add(ChatHistoryQueryInquire.class); } /** * Process an input line * @param line the line to be broadcast as a chat message. * @return true if processed successfully */ @Override protected boolean processConsoleLine(String line) { try { // If the line starts with '!' this is a command line if (line.startsWith("!")) { String command[] = line.trim().split("s+", 2); switch (command[0].toLowerCase()) { case JOIN: // if room name is provided join the room if (command.length > 1) joinRoom(command[1]); break; case LEAVE: // If the user is in a room, leave it leaveRoom(null); break; case LOGIN: login(); break; case LOGOUT: logout(); break; case REGISTER: register(); break; case HISTORY: if (myName == null) System.out.println("You must log in before requesting history."); else if (!getHistory(command.length < 2 ? null : command[1])) System.out.println("Invalid command argument"); break; } return true; } // If the line starts with '>' split the line on the colon (':') and send the test // after the colon as a private message to the user with the name before the colon. if (line.startsWith(">")) { String cmd[] = line.substring(1).split(":"); if (cmd.length < 2) return true; String destination = findUser(cmd[0]); if (destination == null) return true; // If didn't do yet, create an orphan Chatter object for sending private messages, // then prepare a ChatMessage object for sending private messages if (privateMessage == null) { privateMessage = createTopLevelObject(Chat.class, null); privateMessage.setName(myName); } // Set message and commit to destination privateMessage.setMessage(cmd[1]); privateMessage.setTime(ZDate.now()); privateMessage.commit(destination); return true; } // If this is a normal message, make sure the user is in a room and broadcast the message if (message != null) { message.setMessage(line); message.setTime(ZDate.now()); message.commit(); } return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if a data object does not contain ObjectCode static field or the field is not public. // In this case, send a command exception message. sendExceptionMail(ex, "Cannot instantiate a data object class", null, false); return false; } } public String getMyName() { return myName; } public ZDate getBirthday() { return birthday; } /** * Get user name and password and log in. * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. * @throws NoSuchFieldException If LoginQuery does not define ObjectCode static field. * @throws IllegalAccessException If LoginQuery.ObjectCode is not public. */ private PrintStream login() throws NoSuchFieldException, IllegalAccessException { String username; String password; // Check if already logged in if (myName != null) return System.out.printf("You are already logged in as %s. To log in with another name log out first.n", myName); // Create a login query LoginQuery query = createQuery(LoginQuery.class); while(true) { // Get user name System.out.printf("(type Enter to exit login)n"); username = readConsoleLine("User name:"); if (username.isBlank()) return exitLogin(); do { // Get password password = readConsoleLine("Password:"); if (password.isBlank()) return exitLogin(); // Post the login query query.setNewUser(false); query.setName(username); query.setPassword(password); query.post(5 * ZDate.SECOND); // If the query did not expire check login results, if it did, repeat. if (query.waitForReply()) { switch (query.getResult()) { case OK: myName = query.getName(); birthday = query.getBirthday(); return System.out.printf("Logged in successfully as %s.n", myName); case NOT_EXISTS: System.out.printf("User %s does not exist. Please try again.n", username); break; case WRONG_PASSWORD: System.out.printf("Password did not match, please try again.n"); break; } } else return unavailableUserManager(); } while (query.getResult() == LoginQuery.ResultCode.WRONG_PASSWORD); } } /** * Log out if already logged in */ private void logout() { if (myName == null) { System.out.printf("No user is logged in.n"); return; } leaveRoom(null); System.out.printf("%s is logged out.n", myName); myName = null; birthday = null; } private PrintStream register() throws NoSuchFieldException, IllegalAccessException { String username; String password, password2; // Check if already logged in if (myName != null) return System.out.printf("You are already logged in as %s. To register a new user log out first.n", myName); // Create a registration query LoginQuery query = createQuery(LoginQuery.class); while(true) { // Get user name System.out.printf("(type Enter to exit login)n"); username = readConsoleLine("User name:"); if (username.isBlank()) return exitRegistration(); do { // Get password password = readConsoleLine("Password:"); if (password.isBlank()) return exitRegistration(); // Repeat the password password2 = readConsoleLine("Repeat password:"); if (password2.isBlank()) return exitRegistration(); // Check if passwords are equal if (password.equals(password2)) break; System.out.println("Passwords do not match."); } while (true); // Get and parse birth date do { String s = readConsoleLine("Date of birth (yyyy/mm/dd):"); if (s.isBlank()) return exitRegistration(); try { birthday = ZDate.parseTime(s, "yyyy/MM/dd", null); } catch (ParseException ex) { birthday = null; System.out.printf("Invalid date %s. Please try again.n", s); } } while (birthday == null); // Post the registration query query.setNewUser(true); query.setName(username); query.setPassword(password); query.setBirthday(birthday); query.post(5 * ZDate.SECOND); // If the query did not expire check login results, if it did, repeat. if (query.waitForReply()) { switch (query.getResult()) { case OK: myName = username; return System.out.printf("Registered and logged in successfully as %s.n", myName); case EXISTS: System.out.printf("User %s already exista. Please try again with another name.n", username); break; } } else return unavailableUserManager(); } } /** * Print a message when login existed by typing a blank Enter * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream exitLogin() { return System.out.printf("Login exitedn"); } /** * Print a message when registration existed by typing a blank Enter * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream exitRegistration() { return System.out.printf("Registration exitedn"); } /** * Print a message when user management is not available * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream unavailableUserManager() { return System.out.printf("No user management entity is available to handle this requestn"); } /** * Join a room after checking that the room exists, its adult setting matches user configuration, * and the user is not already there. If the user is in another room leave that room first. * @param room the new room name * @throws NoSuchFieldException If any of the created data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private synchronized void joinRoom(String room) throws NoSuchFieldException, IllegalAccessException { // User must be logged in if (myName == null) { System.out.printf("You must log in before joining a room.n"); return; } // Check whether room exists and the user is not already in the room ChatRoomConsume chatRoom = getRootObject().getChild(ChatRoomConsume.class, room); if (chatRoom == null) { System.out.printf("Room %s does not existn", room); return; } if (chatter != null && room.equalsIgnoreCase(chatter.getRoomName())) return; // Check if a young chatters is trying to join an adult room if (!chatRoom.canJoin(birthday)) { System.out.printf("You cannot join room %s because it is only for adultsn", room); return; } // Leave current room if any leaveRoom(null); // Join the application to the room ChatApp chatApp = chatRoom.createChild(ChatApp.class, getAppUUID().toString()); // Create a Chatter object for the user that joins the specified room chatter = chatApp.createChild(ChatterConsume.class, myName); chatter.setName(myName); chatter.setBirthday(birthday); chatter.commit(); // let everybody know I joined the room // Prepare a ChatMessage object for sending messages from now on message = chatter.createChild(ChatMessage.class, null); System.out.printf("Joined room %sn", room); } /** * If the user is in the given room, leave it. * @param room the name of the room to be checked if the user is there. If null, leave the current room without checking */ public synchronized void leaveRoom(String room) { if (isSameRoom(room)) { // Leave the room and let everybody know I did DataObject removed = chatter.remove(); if (removed != null) // can be null if the 'chatter' object has already been removed. removed.commit(); // Disconnect the application from the room and let every application know it did removed = chatter.getParent().remove(); if (removed != null) removed.commit(); System.out.printf("Left room %sn", chatter.getRoomName()); chatter = null; message = null; } } /** * Check if the user is in the same room as the parameter * @param room the name of the room to be checked if the user is there. If null, return true * @return true if the user in 'room' or 'room' is null */ public synchronized boolean isSameRoom(String room) { return chatter != null && (room == null || room.equalsIgnoreCase(chatter.getRoomName())); } /** * Get current room name, if any * @return if the user is currently in a room return the room name, otherwise return null. */ public synchronized String getCurrentRoom() { return chatter == null ? null : chatter.getRoomName(); } /** * Find the user whose name equals the parameter * @param name user name to look for * @return If a Chatter object for this user found then return the object ID of its grandparent, which is the UUID of the * application that the given user runs, otherwise return null. * @throws NoSuchFieldException If any of the referred data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private String findUser(String name) throws NoSuchFieldException, IllegalAccessException { FindUserFilter filter = new FindUserFilter(name); Collection<Chatter> users = getRootObject().getFilteredChildren(filter); // If the returned collection is not empty return the object ID (which is a UUID) of the first, // otherwise return null. for (Chatter user : users) { return user.getParent().getObjectID(); } return null; } /** * Execute !history command * @param arg command argument - <n>H or <n>D * @return true if command has been parsed successfully. * @throws NoSuchFieldException If ChatHistoryQuery does not define ObjectCode static field. * @throws IllegalAccessException If ChatHistoryQuery.ObjectCode is not public. */ private boolean getHistory(String arg) throws NoSuchFieldException, IllegalAccessException { // Parse the argument and calculate time ZDate since = null; if (arg != null) { arg = arg.trim().toLowerCase(); int n = ZUtilities.parseInt(arg.substring(0, arg.length() - 1)); if (n <= 0) return false; int unit; switch (arg.substring(arg.length() - 1)) { case "h": unit = ZDate.HOUR; break; case "d": unit = ZDate.DAY; break; default: return false; } since = ZDate.now().add(-n * unit); } // create a query and post it ChatHistoryQuery query = createQuery(ChatHistoryQuery.class); query.setUsername(myName); query.setSince(since); query.post(5 * ZDate.SECOND); return true; } }
The chatter
field
is now defined as ChatterConsume
(line 37). We will see in a second why.
ChatApp
is added
to the produced object list (line 105). This is needed for the routing
procedure of peer applications. For this reason it is also added to the
consumed object list (line 121). The class is also registered in populateObjectFactory() (line 137).
ChatRoom
is now
the grandparent of Chatter
rather
than its immediate parent and we have to consider this change when we extract
the room name from a Chatter
object.
For that purpose we added the getRoomName()
method to ChatterConsume
. You can see
its use in lines 412, 465 and 473. Also in findUser()
(line 484), which is supposed to return an application UUID, we now return the
object ID of the parent of the found Chatter
object rather than its own ID.
When the user joins a room, we need to first create a ChatApp
object under the desired ChatRoom
object and then create a Chatter
object under it. This is done in
lines 424 – 428.
Similarly, when the user leaves the room, we first remove
the Chatter
object and commit the
action so that peer users are notified, then remove the ChatApp
object and commit it so that peer applications know not to
route room messages to this application. This is done in lines 443 – 452.
We removed isMemberOfMyRoom()
all together because we now do it in ChatMessageImp
as we will see in a moment.
Some implementation data objects are also modified. Let’s
see ChatterConsume
:
package org.spiderwiz.tutorial.lesson11.chat; import org.spiderwiz.tutorial.objectLib.Chatter; /** * Consumer implementation of Chatter data object. Notify when a chatter enters or leaves my room. */ public class ChatterConsume extends Chatter { /** * Notify when a chatter enters my room. */ @Override protected void onNew() { if (!isStealth() && ChatMain.getInstance().isSameRoom(getRoomName()) && getName() != null) System.out.printf("%s entered the roomn", getName()); } /** * Notify when a chatter leaves my room. If the chatter is myself leave the room gracefully. * @return true to confirm the removal. */ @Override protected boolean onRemoval() { if (ChatMain.getInstance().getAppUUID().toString().equals(getParent().getObjectID())) ChatMain.getInstance().leaveRoom(getRoomName()); else if (!isStealth() && ChatMain.getInstance().isSameRoom(getRoomName()) && getName() != null) System.out.printf("%s left the roomn", getName()); return true; } /** * Utility function to get the room name, which is the ID of the grand parent of the current object. * @return the room name */ String getRoomName() { return getParent().getParent().getObjectID(); } }
As mentioned above, we added the getRoomName()
method (line 34) that returns the object ID of the grandparent of this object. This is used in onNew() (line 15) and onRemoval() (line 24).
Also in onRemoval()
we compare the application UUID against the object ID of the parent of this
object rather than its own ID.
And finally ChatMessageImp
:
package org.spiderwiz.tutorial.lesson11.chat; import java.util.Map; import java.util.UUID; import org.spiderwiz.tutorial.objectLib.ChatApp; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; /** * Consumer implementation of the ChatMessage class */ public class ChatMessageImp extends ChatMessage { /** * Called when a chat message is received. Check if the message has been sent in the room we are in, * then print the sender name and the message. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { if (ChatMain.getInstance().isSameRoom(getParent().getParent().getParent().getObjectID())) { String name = ((Chatter)getParent()).getName(); if (name == null) name = getParent().getObjectID(); System.out.printf("%1$s: %2$sn", name, getMessage()); } return true; } /** * Restrict message sending to: * 1. applications that are not the current application. * 2. applications that are on the same chat room as the current user. * @param appUUID destination application UUID * @param appName destination application name * @param userID destination user ID * @param remoteAddress remote network address * @param appParams application parameters of the destination application * @return true if the object should be sent to this destination */ @Override protected boolean filterDestination(UUID appUUID, String appName, String userID, String remoteAddress, Map<String, String> appParams ) { return !ChatMain.getInstance().getAppUUID().equals(appUUID) && isMemberOfMyRoom(appUUID); } /** * Check if the user running the application whose UUID is given is on the same room as this message is sent to. * @param appUUID Application UUID to check * @return true if the application is in the same room as us. */ private boolean isMemberOfMyRoom(UUID appUUID) { try { // The room is the great-grandparent of the message ChatRoom chatRoom = (ChatRoom)getParent().getParent().getParent(); return chatRoom.getChild(ChatApp.class, appUUID.toString()) != null; } catch (NoSuchFieldException | IllegalAccessException ex) { // Arriving here if a ChatApp does not contain ObjectCode static field or the field is not public. // In this case, send a command exception message. ChatMain.getInstance().sendExceptionMail(ex, "Cannot instantiate ChatApp", null, false); return false; } } }
Here onEvent() (line 20) is slightly changed to fetch
the room name from the great-grandparent of the object rather than from its
grandparent. We also check if the parent’s name
property is null
before printing the
message, because when data is imported from the foreign system we cannot trust
that we get a Chatter
object before
receiving this ChatMessage
object. If
it is null
, we take the user name
from the object ID of the parent (this is always available since, when
receiving a ChatMessage
object whose
parent is not recognized, the framework creates an empty Chatter
object and uses it as the parent of the received object).
Note that if the name
property is not
null
then we use it rather than the
object ID because the latter is case insensitive while we prefer to display a
case sensitive name.
As mentioned above we have moved isMemberOfMyRoom()
from ChatMain
to this class (line 54). The method is used in filterDestination() (line 42). The method checks
if the great-grandparent of the current object, which is a ChatRoom
object, has a child whose object ID equals the appUUID
parameter. Recall that ChatRoom
children are all the ChatApp
objects that are in the room.
We are done with all the necessary modifications to the chat
application. Let’s build the Chat Room
Bridge. Here is BridgeMain
:
package org.spiderwiz.tutorial.lesson11.bridge; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ChatApp; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class BridgeMain extends ConsoleMain { private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "bridge.conf"; private static final String APP_NAME = "Chat Room Bridge"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version /** * Class constructor with constant parameters. Call super constructor and create the user map. */ public BridgeMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { new BridgeMain().init(); } /** * @return the list of produced objects */ @Override protected String[] getProducedObjects() { return new String[]{ Chatter.ObjectCode, ChatMessage.ObjectCode, ObjectCodes.RawExport // predefined code for exporting raw data to a remote service }; } /** * @return the list of consumed objects */ @Override protected String[] getConsumedObjects() { return new String[]{ Chatter.ObjectCode, ChatMessage.ObjectCode, ObjectCodes.RawImport // predefined code for importing raw data from a remote service }; } /** * Add implementation classes to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(ChatRoom.class); factoryList.add(ChatApp.class); factoryList.add(ChatterBridge.class); factoryList.add(ChatMessageBridge.class); } /** * Process an input line - nothing to process in our case. * @param line The input line * @return true */ @Override protected boolean processConsoleLine(String line) { return true; } }
The goal of the bridge is to import foreign data, convert it
to Spiderwiz native Data Objects and distribute them. So the application
produces (line 40) Chatter
and ChatMessage
objects. (You can see that
it also produces ObjectCodes.RawExport. We will show its use later
but for now you can safely ignore it).
Similarly, the bridge exports native Data Objects to the foreign system, so it consumes (line 52) the same objects (plus ObjectCodes.RawImport that will be explained later and may be ignored now).
The method populateObjectFactory() (line 65) registers ChatRoom
, ChatApp
and the implementation classes ChatterBridge
and ChatMessageBridge
. Nothing to do in processConsoleLine()
(line 79).
The interesting work is done in the implementation classes.
Here is ChatterBridge
:
package org.spiderwiz.tutorial.lesson11.bridge; import org.spiderwiz.core.ImportHandler; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.zutils.ZDate; import org.spiderwiz.zutils.ZUtilities; /** * Extend Chatter to implement import/export from/to an external chat system. * Data from the external system is received as text lines, each in the form of: * <room name>,<chatter name>,<message> * The line can report that a user joined a room with: * !join,<room name>,<chatter name> * When a user leaves a room the following line reports it: * !leave,<room name><chatter name> */ public class ChatterBridge extends Chatter { /** * Parse import lines and handle them if they are either !join or !leave command * @param data the imported data line. * @param channel the handler of the channel the data is imported from. Shall be "bridge". * @param ts the timestamp attached to the data by the channel handler. * @return the key hierarchy of the imported object - room name then application UUID then user name, or null * if this is not a !join command * @throws Exception */ @Override protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception { // check if the import handler name is "bridge" if (!"bridge".equalsIgnoreCase(channel.getName())) return null; // parse the line String elements[] = data.toString().split(",", 3); if (elements.length < 3) return null; switch(elements[0]) { case "!leave": // Import the removal then proceed as if joined. if (remove() == null) return null; case "!join": // Set user name and return the key list of the joining (or leaving) user setName(elements[2]); return new String[] {elements[1], getMyUUID(), getName()}; } // In all other cases return null return null; } /** * Export a !join or !leave command * @param channel the handler of the channel the object will be exported to. Shall be "bridge". * @param newID null if this object is active, empty string if it has been removed, non-empty string if it has been renamed. * @return the serialized object to export */ @Override protected String exportObject(ImportHandler channel, String newID) { // Do not export my own object if (BridgeMain.getInstance().getAppUUID().equals(getOriginUUID())) return null; // check if the import handler name is "bridge" if (!"bridge".equalsIgnoreCase(channel.getName())) return null; // determine if we need to export a "!join" or a "!leave" command, then generate the command String command = newID == null ? "!join" : "!leave"; return ZUtilities.concatAll(",", command, getRoomName(), getName()); } /** * @return true to make Chatter as urgent as ChatMessage, otherwise object imported from a sequential file might be delivered * in the wrong order. */ @Override protected boolean isUrgent() { return true; } /** * When a new chatter in a room is encountered, export it */ @Override protected void onNew() { commit(""); } /** * When a chatter leaves a room, export the command * @return true to confirm the removal. */ @Override protected boolean onRemoval() { // Done exactly as oneNew(). exportObject() detects whether this is a join or a leave case. commit(""); return true; } /** * @return the current application UUID as a string */ private String getMyUUID() { return BridgeMain.getInstance().getAppUUID().toString(); } /** * Utility function to get the room name, which is the ID of the grand parent of the current object. * @return the room name */ String getRoomName() { return getParent().getParent().getObjectID(); } }
This is where Chatter
objects are imported and exported. Import is done in importObject() (line 29). Its parameters are the imported raw data
, the channel
through which the data is received and a time stamp. We first check if the channel name is "bridge"
. 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.
We then parse the line. Lines that are relevant to Chatter
objects start with either "!join"
or "!leave"
. The first is received when a chatter joins a room. In both cases we set the name
property of the current object and return a string array that contains the object key list starting from the root – room name that identifies the grandparent ChatRoom
object, the current application UUID that identifies the parent ChatApp
object and the user name that identifies the current object. The difference is that In the “!leave”
case we first call remove() on the object to mark it for deletion. That’s it!
Export is done in exportObject() (line 60). First we verify that we do not bounce an imported object by comparing the application UUID to the object’s origin UUID. We then make sure that the export channel is "bridge"
. Finally we construct either a "!join"
or "!leave"
command depending on the value of the newID
parameter. We return the constructed string and we are done.
The reason we override isUrgent() to return true
(line 79) is that, if you still remember, ChatMassage.isUrgent()
returns true
, and if both classes do not return the same value then we cannot guarantee the order by which the imported objects are distributed.
To activate export we need to override onNew() (line 87) and onRemoval() (line 96). In both cases we call commit(“”), which is what activates export (and native distribution). We use the empty string parameter because if we used the non-parameterized commit() then the object would be bounced to the current application, and that would trigger an endless (until heap memory is exploited) recursive call to onNew()
or onRemoval()
. The empty string means “do not send it to any consumer but still export it”.
The other implementation class is ChatMessageBridge
:
package org.spiderwiz.tutorial.lesson11.bridge; import java.util.Map; import java.util.UUID; import org.spiderwiz.core.ImportHandler; import org.spiderwiz.tutorial.objectLib.ChatApp; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.zutils.ZDate; import org.spiderwiz.zutils.ZUtilities; /** * Implementation class of ChatMessagee for Chat Room Bridge. * Data from and to the external system is delivered as text lines, each in the form of: * <room name>,<chatter name>,<message> */ public class ChatMessageBridge extends ChatMessage { /** * If this is not a bounced message that was imported by this application then commit it to itself in order to export it. * @return true; */ @Override protected boolean onEvent() { commit(""); return true; } /** * Parse import lines of the form <room name>,<chatter name>,<message> and use the elements to feed * ChatMessage objects to the system. * @param data the imported data string. * @param channel the handler of the channel the data is imported from. * @param ts the timestamp attached to the data by the channel handler. * @return the key hierarchy of the imported object - room name then application UUID * @throws Exception */ @Override protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception { // check if the import handler name is "bridge" if (!"bridge".equalsIgnoreCase(channel.getName())) return null; // Parse the line and process it String elements[] = data.toString().split(",", 3); if (elements.length < 3) return null; switch(elements[0]) { case "!join": case "!leave": return null; } setMessage(elements[2]); setTime(ts); // Return the key sequence return new String[] {elements[0], BridgeMain.getInstance().getAppUUID().toString(), elements[1]}; } /** * Serialize the object for exporting. * @param channel the handler of the channel the object will be exported to. * @param newID null if this object is active, empty string if it has been removed, non-empty string if it has been renamed. * In this case it is always null because ChatMessage is disposable. * @return the serialized object to export in the form <room name>,<chatter name>,<message>. * If the object is imported we don't want to bounce it so we return null; */ @Override protected String exportObject(ImportHandler channel, String newID) { // Don't bounce imported objects if (BridgeMain.getInstance().getAppUUID().equals(getOriginUUID())) return null; // check if the import handler name is "bridge" if (!"bridge".equalsIgnoreCase(channel.getName())) return null; // serialize String room = getParent().getParent().getParent().getObjectID(); String chatter = ((Chatter)getParent()).getName(); return ZUtilities.concatAll(",", room, chatter, getMessage()); } /** * Make sure the object is delivered to the right destinations, i.e. to chatters in the same room or to other bridge applications. * @param appUUID Application UUID of the destination * @param appName Application name of the destination. Not relevant here. * @param userID User ID used for establishing communication. N/A. * @param remoteAddress Remote address of the destination application. N/A. * @param appParams application parameter map of the destination. If it includes "role" -> "bridge" mapping * then deliver all messages with no further filtering. * @return true to approve distribution to this application, false to deny it. */ @Override protected boolean filterDestination(UUID appUUID, String appName, String userID, String remoteAddress, Map<String, String> appParams) { try { // Check if the destination is for the same room ChatRoom chatRoom = (ChatRoom)getParent().getParent().getParent(); return chatRoom.getChild(ChatApp.class, appUUID.toString()) != null; } catch (NoSuchFieldException | IllegalAccessException ex) { // Arriving here if ChatApp does not contain ObjectCode static field or the field is not public. // In this case, send an exception message. BridgeMain.getInstance().sendExceptionMail(ex, "Cannot instantiate ChatApp class", null, false); return false; } } }
Here we do pretty much the same thing. In onEvent() (line 25) we call commit(“”) to activate export (avoiding recursive
commits). In importObject() (line 40) we verify that the import channel is “bridge”
, parse the imported data (that is supposed to be a text
string), and if it is a chat message we store the values in the current object
and return its hierarchy key as a string array – [room name, current
application UUID, user name].
In exportObject() (line 70) we verify that we do not
bounce an imported object and that the export channel is “bridge”
, then we construct the export message from the object
properties and return it.
The filterDestination() method (line 96) is called
during the distribution process of the object that was imported by importObject()
. We override it to
restrict object routing to applications whose users are in the same room as the
object, just like the chat applications do.
This completes our coding mission. And where is the actual communication with the foreign system? Nice guess! In the configuration file of course. Here it is:
[log folder]/tests/Bridge/Logs [producer-1]ip=localhost;port=10001 [import-1]infile=/tests/Remote/ImportIn.txt;outfile=/tests/Remote/ImportOut.txt;name=bridge
You can see one new property – import-1
. In this case we use text files for input and output, but
you can also use other configurations. You can also write
your own import handler plugin to extend the default one or to
support other types of foreign data.
Note that this property includes the parameter name=bridge
. This gives the input
channel the “bridge”
name that is
checked a few times in the code described above.
One more thing for your attention – when you run the
application with this test scenario you will see that a new folder is created
under the configured log folder named imports
.
The folder will contain a sub folder named bridge
.
See Spiderwiz Logging System for more details.
Import-Export delegation
We still owe you an explanation about the ObjectCodes.RawExport
and ObjectCodes.RawImport
constants that we
use in BridgeMain.getProducedObjects()
and BridgeMain.getConsumedObjects()
respectively. These are not needed for the example we implemented above, but
might be very useful in the following scenario:
When talking about interfacing with an existing system, it happens often that these are production systems that programmers have limited access to. The “bridge” that we built in this lesson requires programming for every specific Data Object that is imported from or exported to the foreign system. If the access to the import channels referred to by the bridge is restricted to modules running on the same production system then the bridge must be collocated on the same system, and then every change to it or the addition of more bridges requires the unwelcome programmer access.
What you would prefer to do in this case is to have a
one-time installation of a “catch all” bridge. This is a Spiderwiz application
that produces the entire raw data as received from the foreign system
encapsulated in RawImport
data
objects. The consumers are the applications that run on a development system,
which parse the data and produce specific data objects. Similarly, the
development applications encapsulate exported data in RawExport
data objects, which are received by the production bridge
and delivered as raw data to the foreign system. This is exactly what the
built-in data objects identified by the said constants do.
In fact they do much more because they completely free the
developers from worrying about the location of the foreign system and whether
it is accessed directly or through a bridge. In the example demonstrated in
this lesson we can have the import-n
configuration property as above, in which case the property define direct
access parameters to the foreign system, or we can omit this property all
together and instead install somewhere a “catch all” bridge that connects to
the foreign system and exchanges the necessary objects with the Chat Room Bridge described here. The
latter works without any change whether configured like in the first or the
second way.
To have the whole picture we will demonstrate the
implementation of the remote bridge. Here is RemoteMain
:
package org.spiderwiz.tutorial.lesson11.remote; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class RemoteMain extends ConsoleMain{ private static final String ROOT_DIRECTORY = ""; private static final String CONFIG_FILE_NAME = "remote.conf"; private static final String APP_NAME = "Remote Import/Export"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version public RemoteMain() { super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { new RemoteMain().init(); } /** * @return a predefined code for importing raw data into a remote service */ @Override protected String[] getProducedObjects() { return new String[]{ObjectCodes.RawImport}; } /** * @return a predefined code for exporting raw data from a remote service */ @Override protected String[] getConsumedObjects() { return new String[]{ObjectCodes.RawExport}; } /** * Process an input line - nothing to process in our case. * @param line The input line * @return true */ @Override protected boolean processConsoleLine(String line) { return true; } }
As you see, this is a barebones ConsoleMain
extension, except that it produces ObjectCodes.RawImport
(line 31) and consumes ObjectCodes.RawExport
(line 29). Nothing else is required since
everything is already built into the framework.
The configuration file is:
[log folder]/tests/Remote/Logs [producer-1]ip=localhost;port=10001 [import-1]infile=/tests/Remote/ImportIn.txt;outfile=/tests/Remote/ImportOut.txt;name=bridge
and if we do import-export through this application then we
can omit the import definition of bridge.conf
:
[log folder]/tests/Bridge/Logs [producer-1]ip=localhost;port=10001
The magic of SpiderAdmin
If you run the examples of this lesson you may discover that they do not work as you might except. The problem is that our tests use local files for accessing “foreign data”, and the input file is opened and read during application initialization. Most probably, when the file is read, handshake procedures on other communication channels have not yet been completed and therefore the processed file data cannot be delivered to other applications.
We want to activate file processing only after all other communication channels are fully established. This is done easily with the “hot reconfiguration” feature of SpiderAdmin. Here is how it works:
Initially when we run Chat Room Bridge we comment out the definition of the import channel, like this:
[log folder]/tests/Bridge/Logs [producer-1]ip=localhost;port=10001 [-import-1]infile=/tests/Remote/ImportIn.txt;outfile=/tests/Remote/ImportOut.txt;name=bridge
We run all the services that constitute the chat system, assuring that My Hub is connected to SpiderAdmin as in Lesson 3. When we surf to SpiderAdmin and log into the service, we should see something like this:
Look for Chat Room Bridge In the Applications table and click it. You should see this:
You see a row of buttons under the page title. Click Update Configuration and you will see this:
Remove the commenting minus sign in front of the import-1
property, like this:
Then click the ✓ symbol at the top right of the pop-up window.
The Chat Room Bridge console now shows
Connection to ImportIn.txt succeeded
and the imported data from that file shows up in the console of the chat applications that have joined the relevant rooms.
Wait a little while until SpiderAdmin page refreshes or refresh it manually and you will see that the “bridge” import channel appeared in the Import Channels table:
We are very close to having the full picture of the Spiderwiz Programming Model. We will conclude that part of the tutorial in the next lesson when we discuss Lossless Data Objects.