By now you should be acquainted with the major concept of Spiderwiz – the shared Data Object Tree. You know how to create objects and branches, modify them, delete them, rename them and share every action across the service mesh. To complete the picture, this lesson shows how you can easily walk through the entire object tree or a branch of it and perform actions on the traversed data objects.
We use the chat system from the previous lesson as the basis for the demonstration and add the following features that exemplify the use of the newly introduced techniques.
In the chat application:
- A command to list all chatters in all rooms.
- The possibility to send private messages to active chatters by their name.
In the room management application (renamed here Room Control):
- A command that takes young chatters (under the age of 18) out of adult rooms.
To facilitate the demonstration we will add a birthday
property to the Chatter
class defined in the objectLib
package:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; import org.spiderwiz.zutils.ZDate; /** * A data object that representing a chat user 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 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; } /** * @return ChatRoom object code */ @Override protected String getParentCode() { return ChatRoom.ObjectCode; } /** * @return false because objects of this class are persistent */ @Override protected boolean isDisposable() { return false; } }
The added property is birthday
of type ZDate
. ZDate is a Spiderwiz extension of Date that lets you do almost everything you can think about with dates and times. We will see its use later.
Data Object Filters
This lesson introduces a new Spiderwiz technique – Data Object Filters. A Spiderwiz filter
is an extension of the Filter class that lets you traverse the data object tree, or a branch of it, in order to collect data objects of a specific type that meet a specific condition. You apply the filter by calling DataObject.getFilteredChildren() on the parent of the branch you want to filter. If you want to apply the filter on the entire tree, call it on the root object.
Filtering works as follows. First, the object being filtered is checked for the existence of direct children of the specified type. If there are any, filterObject() is applied on them and the children that return true
are collected. If the filtered object does not have direct children of the specified type, filterParent() is applied on every child of the filtered object, regardless of its type, and if the method returns true
then the filtering mechanism is applied on the child recursively.
In this lesson we use the following Filter extensions:
- CatchAllFilter – part of the Spiderwiz framework. It does what its name says – collects all the descendants of the specified type of the object it is applied on. We use it for listing all the users in all chat rooms.
FindUserFilter
– implemented here and used to find a user by name for sending a private message.AdultRoomFilter
– implemented here and used to find all users that are not adults in all adult chat rooms.
We will come back to this stuff in a short while.
Let’s see now the chat application – Chat Room Client. The input line syntax is extended as follows:
- Type
!list
to list all users in all rooms. - Type
>username:message
to send a private message to the specified user.
Here is ChatMain
class:
package org.spiderwiz.tutorial.lesson6.chat; import java.text.ParseException; import java.util.Collection; import java.util.List; import java.util.UUID; import org.spiderwiz.admin.xml.OpResultsEx; import org.spiderwiz.core.CatchAllFilter; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.Chat; 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.zutils.ZDate; /** * 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 = "Z1.01"; // Version Z1.01: Initial version 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 LIST = "!list"; // list all users in all rooms private String myName; // Get this from the configuration file private Chatter 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(); } /** * Before starting Spiderwiz engine (but after reading program configuration) get the chatter name, * then print user instructions to the console. * @return true if the name is provided, false if not. */ @Override protected boolean preStart() { myName = getConfig().getProperty("my name"); if (myName == null || myName.isBlank()) { System.out.println("Chatter name has not been defined"); return false; } System.out.printf( "Hello %s. Welcome to the dynamic chat system.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 list all users type !list.n" + "To exit type 'exit'.n", myName ); return true; } /** * @return the list of produced objects - Chatter, Chat and ChatMessage */ @Override protected String[] getProducedObjects() { return new String[]{ Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode }; } /** * @return the list of consumed objects - ChatRoom, Chatter, Chat and ChatMessage */ @Override protected String[] getConsumedObjects() { return new String[]{ ChatRoom.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.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(ChatterConsume.class); factoryList.add(ChatConsume.class); factoryList.add(ChatMessageImp.class); } /** * 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 { ChatRoom chatRoom = getRootObject().getChild(ChatRoom.class, room); if (chatRoom == null) { System.out.printf("Room %s does not existn", room); return; } if (chatter != null && room.equalsIgnoreCase(chatter.getParent().getObjectID())) return; leaveRoom(null); // Create a Chatter object for the user that joins the specified room chatter = chatRoom.createChild(Chatter.class, getAppUUID().toString()); chatter.setName(myName); chatter.setBirthday(getBirthday()); 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)) { DataObject removed = chatter.remove(); // leave the room if (removed != null) // can be null if the 'chatter' object has already been removed. removed.commit(); // let everybody know I did System.out.printf("Left room %sn", chatter.getParent().getObjectID()); chatter = null; message = null; } } /** * Get the user birthday from the configuration file. The birthday property should be in the format: * [birthday]yyyy/mm/dd. If the property is not defined or the value is not in the correct format then null * is returned. * @return the birthday as a ZDate object or null if not defined or cannot be parsed. */ private ZDate getBirthday() { try { return ZDate.parseTime(getConfig().getProperty("birthday"), "yyyy/MM/dd", null); } catch (ParseException ex) { return 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.getParent().getObjectID())); } /** * Check whether the user running the application whose UUID is given is on the same room as us. This is done by getting * our current ChatRoom object and see if it has a child with that ID. * @param appUUID Application UUID to check * @return true if the application is in the same room as us. */ public synchronized boolean isMemberOfMyRoom(UUID appUUID) { try { if (chatter == null) return false; ChatRoom chatRoom = (ChatRoom)chatter.getParent(); return chatRoom != null && chatRoom.getChild(Chatter.class, appUUID.toString()) != null; } 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; } } /** * Apply a filter on the entire object tree to get a collection of all users currently in any room, * then iterate through the collection to print out user names with their rooms. * @throws NoSuchFieldException If any of the created data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private void listAllUsers() throws NoSuchFieldException, IllegalAccessException { CatchAllFilter<Chatter> filter = new CatchAllFilter<>(Chatter.class); Collection<Chatter> allUsers = getRootObject().getFilteredChildren(filter); allUsers.forEach((user) -> { System.out.printf("%1$s in room %2$sn", user.getName(), user.getParent().getObjectID()); }); } /** * 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 its object ID, 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.getObjectID(); } return null; } /** * Called when application configuration has been changed. If the user is in a room and the configured birthday * has been changed set the new value in the Chatter object and commit it. * @return the value returned by the super method. */ @Override public OpResultsEx reloadConfig() { OpResultsEx opResult = super.reloadConfig(); if (chatter != null) { ZDate bd = getBirthday(); if (bd == null && chatter.getBirthday() != null || bd != null && !bd.equals(chatter.getBirthday()) ) { chatter.setBirthday(bd); chatter.commit(); } } return opResult; } /** * 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 LIST: listAllUsers(); 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.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.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; } } }
We will concentrate on the changes from Lesson 5, as follows:
A new command label named LIST
is defined in line 27.
In prestart() (line 72), the user instructions are updated.
There is a small change in joinRoom()
(line 138). We use getBirthday()
to get the configured user’s birth date (see below) and set the birthday
field in the Chatter
object accordingly.
There is also a small change in leaveRoom()
(line 163). Since now a user may be taken out of a room by Room Control application (see below), it may happen that due to synchronization issues the method might try to remove an object that has already been removed. Therefore we check the result of the remove() method (line 165), and only if the returned object is not null
then we commit it in order to propagate the removal to other applications.
The function getBirthday()
(line 180) that was mentioned above gets and parses the birthday
property from the configuration file, which is supposed to be in the format yyyy/mm/dd
. If the property is not available or it is in the wrong format then the method returns null
. Recall that for dealing with date and time objects we use ZDate, so here we use its handy static parseTime() method for parsing the date in the required format.
We are arriving at the action. The method listAllUsers()
(line 224) collects all the Chatter
objects in the system, and, for each object, prints out the user name and the name of the room the user is currently in. This is the place that CatchAllFilter that was mentioned above is used.
The function findUser()
(line 249) gets a user name and returns the UUID of the application used by the user. To do that, it uses FindUserFilter
that was also mentioned above. The filter finds a Chatter
object by user name (we will see the code in a short while). Calling getObjectID() on that object returns the required UUID.
The overriding reloadConfig() method (line 257), which is called when application configuration is changed, calls the super
method to reload the configuration from the file, and then, if the user is active in any room, updates the birthday
field of the active Chatter
object.
Changes from Lesson 5 in processConsoleLine()
(line 277) are as follows:
- The new
!list
command activates a call tolistAllUsers()
(line 293). - When the line starts with ‘>’, which means a private message, the line is split on the ‘:’ symbol, the first component is used in a call to
findUser()
and the result, if notnull
, is used as the destination UUID when committing theChat
object that contains the private message. The message is the second component of the split.
Let’s see the implementations of the data object classes. First ChatConsume
, used for receiving a private message. Nothing has been changed here (except the package) but we copy it for completeness:
package org.spiderwiz.tutorial.lesson6.chat; import org.spiderwiz.tutorial.objectLib.Chat; /** * Consumer implementation of the Chat class */ public class ChatConsume extends Chat{ /** * Called when a private message is received. Print the sender name followed by a colon, then the message. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { System.out.printf("%1$s: %2$sn", getName(), getMessage()); return true; } }
For in-room massaging we use ChatMessageImp
:
package org.spiderwiz.tutorial.lesson6.chat; import java.util.Map; import java.util.UUID; import org.spiderwiz.tutorial.objectLib.ChatMessage; 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().getObjectID())) { System.out.printf("%1$s: %2$sn", ((Chatter)getParent()).getName(), 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) && ChatMain.getInstance().isMemberOfMyRoom(appUUID); } }
This is almost identical to the version we used in Lesson 5, except that we do not need to call setLastSender()
because we use the chatter name for private messaging.
ChatterConsume
handles events related to specific chatters entering a room and leaving it:
package org.spiderwiz.tutorial.lesson6.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() { // The room is the parent of this object if (ChatMain.getInstance().isSameRoom((getParent().getObjectID()))) 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(getObjectID())) ChatMain.getInstance().leaveRoom(getParent().getObjectID()); else if (ChatMain.getInstance().isSameRoom(getParent().getObjectID())) System.out.printf("%s left the roomn", getName()); return true; } }
Note the added lines 26 and 27. They are needed because a user can be taken out of a room by an external entity (Room Control). We check whether the chatter taken out of the room is the user itself, and if so we call ChatMain.leaveRoom()
to complete the operation and print an appropriate message. Otherwise we print a message that a user left the room as before.
ChatRoomConsume
is almost identical to the previous version except that here we do not deal with changes in the adult setting of the rooms:
package org.spiderwiz.tutorial.lesson6.chat; import org.spiderwiz.tutorial.objectLib.ChatRoom; /** * Consumer implementation of the ChatRoom class */ public class ChatRoomConsume extends ChatRoom { /** * If the chat room has been removed and the user is there, leave the room * @return true to confirm the removal. */ @Override protected boolean onRemoval() { ChatMain.getInstance().leaveRoom(getObjectID()); return true; } /** * Notify on room name change if the user is in the renamed room * @param oldID */ @Override protected void onRename(String oldID) { if (ChatMain.getInstance().isSameRoom(getObjectID())) System.out.printf("Your room %1$s was renamed %2$sn", oldID, getObjectID()); } }
It remains to see FindUserFilter
:
package org.spiderwiz.tutorial.lesson6.chat; import org.spiderwiz.core.Filter; import org.spiderwiz.tutorial.objectLib.Chatter; /** * A data object tree filter that finds a user with a specific name */ public class FindUserFilter extends Filter<Chatter> { private final String name; /** * Class constructor * @param name // name of the user we are looking for * @throws NoSuchFieldException If any of the created data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ public FindUserFilter(String name) throws NoSuchFieldException, IllegalAccessException { super(Chatter.class); this.name = name; } /** * Returns true if the checked user is the one we are looking for * @param user a Chatter object to check * @return true if user.getName() equals this.name */ @Override protected boolean filterObject(Chatter user) { return user.getName().equalsIgnoreCase(name); } }
Being familiar with Spiderwiz Filter, this is really straightforward. The class extends Filter<Chatter>
. Its constructor gets the name of the user we are looking for as a parameter. The class overrides filterObject()
to return true
when the Chatter
object the filter is looking at is the one it is looking for.
We are done with the chat application. Before moving on to the other application take a look at some configuration files:
[application name]Chat 1 [log folder]/tests/Chat1/Logs [producer-1]websocket=localhost:90/MyHub [my name]FERRANDO
[application name]Chat 2 [log folder]/tests/Chat2/Logs [producer-1]websocket=localhost:90/MyHub [my name]GUGLIELMO [birthday]2003/07/04
[application name]Chat 3 [log folder]/tests/Chat3/Logs [producer-1]websocket=localhost:90/MyHub [my name]DON ALFONSO [birthday]2002/07/14
We can see that FERRANDO and GUGLIELMO would be considered too young for participating in an adult chat room (the first configured no birth date, the second is under 18 year old), while DON ALFONSO is old enough.
We will look now at the other application — Room Control. Recall that this does what Room Manager did, and additionally its user can type ban
to throw all youngsters out of adult rooms. As usual we start with RoomControlMain
:
package org.spiderwiz.tutorial.lesson6.roomControl; import java.util.Collection; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.Chat; 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 RoomControlMain extends ConsoleMain { private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "room-control.conf"; private static final String APP_NAME = "Room Control"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private static final String CREATE = "create"; // create chat room command private static final String DELETE = "delete"; // delete chat room command private static final String MODIFY = "modify"; // modify chat room command private static final String RENAME = "rename"; // rename chat room command private static final String BAN = "ban"; // take young chatters out of adult rooms /** * Class constructor with constant parameters. */ public RoomControlMain() { 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 RoomControlMain().init(); } /** * Print out usage instructions at start up * @return true */ @Override protected boolean preStart() { System.out.printf( "To create a new room: type 'create <room name>'.n" + "Append a plus sign (+) to the name if you want to create an adult only room.n" + "To delete a room type 'delete <room name>'.n" + "To modify the adult categorization of a room:n" + "Type 'modify <room name>+' if you want to make it an adult room.n" + "Type 'modify <room name>' if you want to remove the adult categorization.n" + "To rename a room type 'rename <room name>=<new name>'.n" + "To take young chatters out of adult rooms type 'ban'.n" + "To exit type 'exit'.n" ); return true; } /** * @return the list of produced objects */ @Override protected String[] getProducedObjects() { return new String[]{ ChatRoom.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode }; } /** * @return the list of consumed objects */ @Override protected String[] getConsumedObjects() { return new String[]{Chatter.ObjectCode}; } /** * Add ChatRoom class 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(Chatter.class); factoryList.add(Chat.class); } /** * Create a chat room * @param roomName the name of the room * @param adult true if this is an adult room * @throws NoSuchFieldException If ChatRoom does not define ObjectCode static field. * @throws IllegalAccessException If ChatRoom.ObjectCode is not public. */ private void createRoom(String roomName, boolean adult) throws NoSuchFieldException, IllegalAccessException { // Create a new room if does not exist (ignore if it does) ChatRoom room = createTopLevelObject(ChatRoom.class, roomName); room.setAdult(adult); room.commit(); System.out.printf("Room %1$s(%2$s) createdn", roomName, adult ? "adult" : "unrestricted"); } /** * Delete a room * @param room The ChatRoom object to delete. */ private void deleteRoom(ChatRoom room) { DataObject removed = room.remove(); if (removed != null) removed.commit(); System.out.printf("Room %1$s deletedn", room.getObjectID()); } /** * Modify the 'adult' setting of a room * @param room The ChatRoom object to modify * @param adult The value of 'adult' property to set. */ private void modifyRoom(ChatRoom room, boolean adult) { room.setAdult(adult); room.commit(); System.out.printf("Room %1$s set to %2$sn", room.getObjectID(), adult ? "adult" : "unrestricted"); } private void renameRoom(ChatRoom room, String newName) { if (!newName.equalsIgnoreCase(room.getObjectID())) { DataObject renamed = room.rename(newName); if (renamed == null) System.out.printf("Cannot rename to already existing room name %sn", newName); else { renamed.commit(); System.out.printf("Room %1$s was renamed %2$sn", room.getObjectID(), newName); } } } /** * Collect all users whose age is under 18 that are in an adult chat room and get them out of the room. * @throws NoSuchFieldException If Chatter does not define ObjectCode static field. * @throws IllegalAccessException If Chatter.ObjectCode is not public. */ private void ban() throws NoSuchFieldException, IllegalAccessException { AdultRoomFilter filter = new AdultRoomFilter(); Collection<Chatter> users = getRootObject().getFilteredChildren(filter); if (users.isEmpty()) System.out.printf("There are no young users in any adult room.n"); users.forEach((user) -> { System.out.printf("%1$s has been taken out of room %2$sn", user.getName(), user.getParent().getObjectID()); DataObject removed = user.remove(); if (removed != null) removed.commit(); }); } /** * Process an input line * @param line An input line that contains a room management command * @return true if processed successfully */ @Override protected boolean processConsoleLine(String line) { try { // Trim line and split to command / parameter. // Then check if there is a parameter and whether it contains the adult symbol. // If there is not parameter, ignore the command. String command[] = line.trim().split("s+", 2); String name[] = null; boolean adult = false; String roomName = null; ChatRoom room = null; if (command.length >= 2) { name = command[1].split("+", -1); adult = name.length > 1; // now split for 'rename' command name = name[0].split("="); roomName = name[0]; // If exists, get the ChatRoom object room = getRootObject().getChild(ChatRoom.class, roomName); } // Switch by command and process it switch (command[0].toLowerCase()) { case CREATE: // Create a new room if does not exist (ignore if it does) if (roomName != null && room == null) createRoom(roomName, adult); break; case DELETE: // Delete the room if exists if (room != null) deleteRoom(room); break; case MODIFY: // Modify the 'adult' field if the room exits if (room != null) modifyRoom(room, adult); break; case RENAME: // Rename the room if exists. The rename command is 'rename <old name>=<new name>' if (room != null && name != null && name.length > 1) renameRoom(room, name[1]); break; case BAN: ban(); break; } return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if any 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; } } }
The new ban
command is labeled in line 24 and included in the user instructions printed in preStart() (line 47).
The method getProducedObjects() (line 66) returns the object codes of Chatter
and Chat
in addition to ChatRoom
that was produced by Room Manager. Chatter
is produced because a user is taken out of a room by removing a Chatter
child from its ChatRoom
parent and committing the removal to other applications. You may assert that this is destruction rather than production, but in the Spiderwiz jargon, to produce an object means to produce messages that relate to the object, and this is exactly what the application does.
Chat
is produced when Room Control sends messages to chatters to notify them when they are taken out of a room.
The application consumes Chatter
(line 78), as it needs to know which chatters are in which rooms.
The method populateObjectFactory() registers the base classes used by the application. There is no need for any implementation extension.
Next come methods that perform the application actions – createRoom()
(line 101), deleteRoom()
(line 113), modifyRoom()
(line 125), renameRoom()
(line 131) and ban()
(line 148). All but the last are copies of the code of Room Manager from Lesson 5. The last one is new. The method uses AdultRoomFilter
(code below) to collect the Chatter
objects of all the young chatters that are in adult rooms. If there are none, it prints an appropriate message. If there are some then for each of them a message is printed, the object is removed and the removal is propagated to other applications by commit().
The overriding method processConsoleLine()
(line 167) gets input lines, parses them and calls the appropriate method to execute them.
It remains to see AdultRoomFilter
that finds young chatters in adult rooms:
package org.spiderwiz.tutorial.lesson6.roomControl; import org.spiderwiz.core.DataObject; import org.spiderwiz.core.Filter; import org.spiderwiz.tutorial.objectLib.Chat; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.zutils.ZDate; /** * A data object tree filter that collects all users whose age is under 18 that are in an adult chat room. */ public class AdultRoomFilter extends Filter<Chatter> { private final Chat adminMessage; /** * Class constructor. Call the super constructor with the appropriate class type and prepare a Chat object * for sending private messages to users that are banned from an adult room. * @throws NoSuchFieldException If Chatter does not define ObjectCode static field. * @throws IllegalAccessException If Chatter.ObjectCode is not public. */ public AdultRoomFilter() throws NoSuchFieldException, IllegalAccessException { super(Chatter.class); adminMessage = RoomControlMain.createTopLevelObject(Chat.class, null); adminMessage.setName("Room Administrator"); adminMessage.setMessage("You need to be at least 18 year old to chat in an adult room. You will be taken out of the room"); } /** * Filter out ChatRoom objects that are not defined as "adult" * @param parent an ancestor of Chatter objects that are scanned by this filter * @return true if 'parent' is not a ChatRoom object or it is and is defined as "adult" */ @Override protected boolean filterParent(DataObject parent) { if (parent instanceof ChatRoom) return ((ChatRoom)parent).isAdult(); return true; } /** * Check whether the given chatter is under 18 years old or did not configure a birthday. * In this case send a private message to the chatters to let them know that they are taken out of the room. * @param chatter a Chatter object describing a chat user. * @return true if the given chatter is under 18 years old or did not configure a birthday. */ @Override protected boolean filterObject(Chatter chatter) { if (ZDate.now().diffMonths(chatter.getBirthday()) < 18 * 12) { adminMessage.commit(chatter.getObjectID()); return true; } return false; } }
This filter applies to Chatter
objects so it extends Filter<Chatter> . Its constructor (line 24) calls the super
constructor and then prepares a Chat
object for sending notifications to users who are going to be removed from a chat room.
The filter is only interested in adult chat rooms, so filterParent() (line 27) is overridden to return true
only if the filtered parent is marked as an adult room.
Within adult rooms, the filter needs to collect users whose age is either not specified or is under 18 year old. This is done by implementing filterObject() (line 50). The date of birth is a field in Chatter
retrieved by getBirthday()
, and is checked for age by calling ZDate.now() to get an object representing the current time and date, calculating the difference in months from the date of birth by calling diffMonths() and checking whether it is not less than 12 * 18 months.
The method also sends a message to the found users using the Chat
object that was prepared by the class constructor.
We are done with coding Room Control. Nothing is special about its configuration file:
[log folder]/tests/RoomControl/Logs [producer-1]ip=localhost;port=10001
Now that you are well acquainted with the essentials of Data Objects, the Data Object Tree, their manipulation and traversal, you are ready to move to the next topic – Query Objects.