In the previous lessons we saw how Spiderwiz worked with a command line application. We connected a producer and a consumer directly over TCP/IP. In this lesson we will create a web application and use it as a hub to connect client producers and consumers over WebSockets. We will use the implementation to demonstrate a simple chat service.
We start by using the IDE to create a Java Web Application project. We call it MyHub
. Since it will act as a WebSocket server we need to add the following dependency, as mentioned in Getting Spiderwiz:
<dependency> <groupId>org.spiderwiz</groupId> <artifactId>spiderwiz-websocket-server</artifactId> <version>1.2</version> </dependency>
Our first class, HubMain
, is an extension of Spiderwiz Main
class. Here it goes:
package org.spiderwiz.tutorial.lesson3; import org.spiderwiz.core.Main; /** * Main class for MyHub. As a hub, we neither produce nor consume any object. */ public class HubMain extends Main{ private static final String ROOT_DIRECTORY = "/tests/MyHub"; private static final String CONFIG_FILE_NAME = "hub.conf"; private static final String APP_NAME = "My Hub"; private static final String APP_VERSION = "Z2.01"; // Version Z2.01: First working version public HubMain() { super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION); } /** * @return an empty list as we produce nothing */ @Override protected String[] getProducedObjects() { return new String[]{}; } /** * @@return an empty list as we consume nothing */ @Override protected String[] getConsumedObjects() { return new String[]{}; } }
As you see, not much. We define a root directory, a configuration file name, application name and version, and return empty lists in getProducedObjects() and getConsumedObjects().
To run the application on our website we need to initialize it from a servlet. Here is RootServlet
class:
package org.spiderwiz.tutorial.lesson3; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; /** * A root servlet for spawning a Spiderwiz application */ @WebServlet(name = "RootServlet", urlPatterns = {"/RootServlet"}, loadOnStartup = 1) public class RootServlet extends HttpServlet { /** * Instantiate and initialize the Spiderwiz runtime * @throws ServletException */ @Override public void init() throws ServletException { super.init(); new HubMain().init(); } /** * Do Spiderwiz cleanup on application termination */ @Override public void destroy() { HubMain.getInstance().cleanup(); super.destroy(); } }
RootServlet
implements a servlet that loads on startup, initializes HubMain
and calls its cleanup() method when the servlet is destroyed.
Actually we are done! If you wonder where all the WebSocket server stuff is, recall that we still have to provide a hub.conf
file. There, in one line (two actually), is where we define our hub as a WebSocket server:
[log folder]Logs [consumer server-1]websocket;ping-rate=30 [producer server-1]websocket;ping-rate=30 [hub mode]yes
Everything is in the definitions of the server as websocket
. There are two property lines that do that, because our hub can be connected by both Producer and Consumer applications, so we define it as a consumer server
for the first and producer server
for the latter.
Note that besides defining each server as “websocket”, we add the parameter “ping-rate=30
”. This is done because many web servers drop WebSocket connections that are idle for too long. The parameter tells the Spiderwiz engine to ping these connections every 30 seconds, therefore keeping them busy so they would not be dropped even if there is no activity for a while.
The last configuration line sets hub mode
to yes
. This is needed because, since the application neither produces nor consumes anything, without it it will neither get nor send any data. The definition as hub mode
tells the system to pass objects of any type through the hub, which routes them between producers and consumers similarly to an IP router.
Go ahead and deploy the project. You will see the following in your web server’s log file:
My Hub ver. Z2.01 (core version Z2.42) has been initiated successfully Producer is listening to consumers on WebSockets Consumer is listening to producers on WebSockets
We are done with the hub server, now to the clients.
The main ingredient of our chat application is the Chat
data object:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; /** * Chat class for Spiderwiz tutorial's lesson 2. */ public class Chat extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHAT"; @WizField private String name; // Contains the name of the chatter @WizField private String message; // Contains a chat message public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } /** * @return null as this is a root object. */ @Override protected String getParentCode() { return null; } /** * @return true as this object is disposable. */ @Override protected boolean isDisposable() { return true; } @Override protected boolean isUrgent() { return true; } }
We define the mandatory ObjectCode
, make objects of this class disposable top-level objects (because we can dispose a message after it has been sent), and define two properties annotated as @WizField
: name
and message
– the first is the name of the chatter and the latter, well, is the chat message. We also mark that this is an urgent object (line 51) because we want chat messages to reach their destination promptly. We place the class in the objectLib
package.
Let’s build now the chat client application. We will implement it as a command line application. We need to start by extending class Main
. But before doing that there is a twist. We want our client to read lines from the console, exit if an input line is “exit”, and do something else if not. Since this mechanism will repeat in many of the next lessons we should make it reusable. To do that, we create a Main
class extension called ConsoleMain
that does the work and place it in the objectLib
package:
package org.spiderwiz.tutorial.objectLib; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import org.spiderwiz.core.Main; /** * Extend Spiderwiz Main class to handle reading input lines from the console by a command line application. */ public abstract class ConsoleMain extends Main{ BufferedReader br; /** * Pass parameters to super constructor * @param rootDirectory the application root directory. * @param configFileName the application configuration file. * @param appName the default application name * @param appVersion the default application version number */ public ConsoleMain(String rootDirectory, String configFileName, String appName, String appVersion) { super(rootDirectory, configFileName, appName, appVersion); } /** * Called after framework initialization. Install a shutdown hook, then loop on reading messages from the console * until "exit" is typed. */ @Override protected void postStart() { // Install a shutdown hook that cleans resources up on termination. Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { cleanup(); } }); // Do post init processing if (!postInit()) return; // Read console lines and process them until "exit" is typed br = new BufferedReader(new InputStreamReader(System.in)); String line; do { line = readConsoleLine(getConsolePrompt()); if ("exit".equalsIgnoreCase(line)) break; } while (processConsoleLine(line)); System.exit(0); } /** * Override this method to return the prompt text to display on the console before reading a line. * @return an empty string by default. */ protected String getConsolePrompt() {return "";} /** * Implement this method to provide console input line processing * @param line a console input line * @return true if program shall continue to process next input line, false if it shall terminate */ protected abstract boolean processConsoleLine(String line); /** * Provides a formatted prompt, then reads a single line of text from the console. * @param fmt A format string as described in Format string syntax. * @param args Arguments referenced by the format specifiers in the format string * @return A string containing the line read from the console */ protected String readConsoleLine(String fmt, Object ... args) { try { System.out.printf(fmt, args); return br.readLine(); } catch (IOException ex) { return null; } } /** * Override this to do something after calling Main.init() and before starting to read console lines * @return true if processing shat continue, false if program shall be aborted. */ protected boolean postInit() { return true; } }
ConsoleMain
is an abstract extension of Main
. It defines four protected methods that can be overridden – getConsolePrompt()
(line 58) returns the prompt text that shall be printed to the console before reading each line. If not overridden, it returns an empty string. The abstract method processConsoleLine()
(line 65) must be implemented to tell what to do with each input line. Additionally readConsoleLine()
(line 73) prints a formatted prompt, then reads a single line of text from the console. The method is called internally from ConsoleMain
and can also be called from derived classes if they need to do their own command line processing. Another method that can be optionally overridden is postInit()
(line 86). It is called after calling Main.init() and before starting to read console lines, therefore extension classes can call it to do something before processing them.
ConsoleMain
overrides Main.postStart() (line 30) to install a shutdown hook, call postInit()
and loop on reading messages from the console and calling processConsoleLine
() to process them until “exit” is typed (or processConsoleLine()
returns false).
Now everything is ready for ChatMain
:
package org.spiderwiz.tutorial.lesson3; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.Chat; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * 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 Client"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private String myName; // Get this from the configuration file private Chat chat = null; // A Chat object for committing chat 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); } /** * 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. * @return true if a chatter 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.println("You are chatting as " + myName + ". Go ahead and type your messages"); return true; } /** * @return the list of produced objects, in this case Chat is the only one. */ @Override protected String[] getProducedObjects() { return new String[]{Chat.ObjectCode}; } /** * @return the list of consumed objects, in this case Chat is the only one. */ @Override protected String[] getConsumedObjects() { return new String[]{Chat.ObjectCode}; } /** * Add ChatConsume implementation 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(ChatConsume.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 not yet done, instantiate a Chat object and set its 'name' field. if (chat == null) { chat = ChatMain.createTopLevelObject(Chat.class, null); chat.setName(myName); } // Set chat message and commit chat.setMessage(line); chat.commit(); return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if the Chat 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 Chat class", null, false); return false; } } }
ChatMain
extends ConsoleMain
. This time the configuration file name that is passed to ChatMain
(and Main
) constructor is entered as a command line argument. This is because we may want to have few instances of the chat client on the same machine, and we don’t want to mix log folders and other properties.
We introduce here (line 49) the usage of Main.preStart()
– a method that is called after the configuration file is loaded and log folders are set, but before Spiderwiz engine is started. Here we get [my name
] property from the configuration file (line 50) and use it as the chatter (owner of the instance) name.
Our application is both a Producer and a Consumer of the Chat
object described above, so both getProducedObjects() and getConsumedObjects() return a list that contains one element – Chat.ObjectCode
.
To handle incoming messages we implement an extension of Chat
called ChatConsume
. We register it in populateObjectFactory()
(line 81).
Lastly, we implement processConsoleLine()
that we inherited from ConsoleMain
(line 91). The argument to this method is the message that shall be sent to the chat group. It works as follows:
- If this is the first message that the application needs to send, use createTopLevelObject() to create a Chat object. Set the
name
field of the object to “my name” taken from the configuration file inpreStart()
. If it is not the first time, use the object created at the first time. - Set the object’s
message
field to the line read from the console. - Commit the object.
You may have noticed that the entire code block is wrapped by try
… catch
commands. This is because createTopLevelObject()
can potentially refer to a data object that does not have a static ObjectCode
field or the field is not declared public
. Although we know that this is not the case here, we need to write code that catches the exceptions, and we use the occasion to demonstrate the use of sendExceptionMail() – a nice Spiderwiz feature that reports exceptions and their stack trace to the console, log files and even in emails sent to configured addresses. See the Javadoc for details.
We saw how the application sent chat messages. It remains to see how it receives and prints them out. This is done in ChatConsume
:
package org.spiderwiz.tutorial.lesson3; import org.spiderwiz.tutorial.objectLib.Chat; /** * Consumer implementation of the Chat class */ public class ChatConsume extends Chat{ /** * Called when a chat 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() { // Do not print out my own messages if (!ChatMain.getInstance().getAppUUID().equals(getOriginUUID())) { System.out.printf("%1$s: %2$s", getName(), getMessage()); System.out.println(); } return true; } }
Quite straightforward. We extend Chat
and override onEvent()
to print sender name and message on the console. The only tricky thing here is that we do not want our own messages to be printed, therefore we use getAppUUID() and getOriginUUID() to compare the receiver application UUID to the sender’s one and skip printing if they are equal.
This concludes our coding work. Again you may wonder where the WebSocket stuff is, and again the answer is that it is where all the interesting things in Spiderwiz happen – in the configuration files.
We will demonstrate the chatting service with three chatters, each running an instance of the chat client. Recall that the client application expects a configuration file name as a command line argument, so each chatter can supply its own configuration file. Let’s see them:
[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
[application name]Chat 3 [log folder]/tests/Chat3/Logs [producer-1]websocket=localhost:90/MyHub [my name]DON ALFONSO
These are explained in the following table:
Property | Description |
---|---|
application name | Used to override the hard coded application name (“Chat Client”) in order to visually differentiate the application instances (in logs etc.). |
log folder | Each instance gets its own log folder. |
producer-1 | Defines a WebSocket client connection. The websocket parameter identifies the WebSocket server application, which in my case resides in localhost:90 . Yours, obviously, may run on another port. The choice of producer in this case is arbitrary, since MyHub defines both a producer server and a consumer server . |
my name | ChatMain.preStart() loads this property (in line 50) as the chatter name. |
One more thing is missing before running the applications. The chat clients connect as WebSocket clients, therefore they need a standalone client dependency. This is mentioned in Getting Spiderwiz and we will repeat it here:
<dependency> <groupId>org.glassfish.tyrus.bundles</groupId> <artifactId>tyrus-standalone-client</artifactId> <version>1.15</version> </dependency>
Everything is ready, let’s shoot it. The following table reflects the chat activity:
Chat 1 | Chat 2 | Chat 3 |
---|---|---|
You are chatting as FERRANDO. Go ahead and type your messages | You are chatting as GUGLIELMO. Go ahead and type your messages | You are chatting as DON ALFONSO. Go ahead and type your messages |
Una bella serenata | FERRANDO: Una bella serenata | FERRANDO: Una bella serenata |
GUGLIELMO: In onor di Citerea | In onor di Citerea | GUGLIELMO: In onor di Citerea |
DON ALFONSO: Sarò anch'io de' convitati? | DON ALFONSO: Sarò anch'io de' convitati? | Sarò anch'io de' convitati? |
Ci sarete, sì signor. | Ci sarete, sì signor. | FERRANDO: Ci sarete, sì signor. |
E che brindisi replicati | E che brindisi replicati | E che brindisi replicati |
GUGLIELMO: E che brindisi replicati | FERRANDO: E che brindisi replicati | FERRANDO: E che brindisi replicati |
exit | exit | exit |
In this example we saw one central hub that was connected to by multiple WebSocket clients. Spiderwiz is not constrained to this topology. You could, for instance, have few hubs connected to each other in TCP/IP and distribute the clients between them. You can also clone several hubs and connect each client to all of them for redundancy. However complex is the topology, applications get the objects they consume, and they get only one copy of each. For details about this and other network and data transfer issues see Lean and Mean – Under the Spiderwiz hood.
The magic of SpiderAdmin
To complete the whole picture we should introduce SpiderAdmin – the administration service that comes free with every Spiderwiz-based application. It is explained in detail in Lesson 14 but we will see a trailer here.
Remember the console message “Include spiderwiz-admin.jar in your project in order to use www.spiderwiz.org/SpiderAdmin
” from the previous lesson? It reminds you that if you want your application to participate in the game then the first thing you have to do is to include the SpiderAdmin dependency in your project.
SpiderAdmin is a complimentary service that is not available on the Maven Central Repository, therefore you need to define the Spiderwiz internal repository in your project:
<project> ... <repositories> <repository> <id>spiderwiz-internal</id> <url>http://spiderwiz.org/repo</url> </repository> </repositories> ... </project>
With this in place you can specify the spider-admin
dependency:
<dependency> <groupId>org.spiderwiz</groupId> <artifactId>spiderwiz-admin</artifactId> <version>2.2</version> </dependency>
Non-Maven users can download spiderwiz-admin.jar
directly by this link.
As mentioned above, this needs to be included in every application that you want to be governed by SpiderAdmin. However, it is enough to connect only one application to the service, and it will be used as a “hole” through which the “colonoscope” penetrates the entire service-mesh and administers every SpiderAdmin-enabled application. In this example the “hole” is My Hub. It works as follows:
First, you must be a registered SpiderAdmin user. Go here and use the panel on the right of the screen to register, or log in if you have done it already. If you prefer not to register at this moment but still want to taste the experience, you can use the predefined “test” user with “test” also as the password. Once logged in, you will be redirected to the SpiderAdmin service page. If you have not connected yet any application to the service (and if you use the “test” account then no other user of that account has), you will see a page like this:
Follow the instructions on the page and copy the displayed configuration property to hub.conf
, which will now look like this:
[log folder]Logs [consumer server-1]websocket;ping-rate=30 [producer server-1]websocket;ping-rate=30 [spideradmin]W6WEMWZEbCRImAMvgf2M2Xtt3tvH [hub mode]yes
Restart My Hub and all the chat applications. Soon the SpiderAdmin service page will refresh to show something like this:
The “Applications” table shows all the active applications, while the “Producers” table shows My Hub that connects directly. You can now surf through the entire service mesh by clicking from application to application. For more information see Lesson 14.
Lesson 3 is concluded. That was quite a long explanation but pretty short code. In the next chapter we will learn about fine grained object routing, which we will use to implement chat rooms.