The Import/Export concept that can be used as a mechanism to bridge between Spiderwiz-based and legacy applications was introduced in Lesson 11. The exercise we did there assumed that the interface with the foreign system was based on serialized data, which the default ImportHandler was capable of processing.
There are situations in which this assumption cannot be
held. The solution in these cases is to extend ImportHandler
. In this lesson we will show how to do that.
The test case that will be demonstrated is the example built in Baeldung’s Guide to the Java API for WebSocket. It is a simple chat application in which clients use a web browser to communicate with a server over WebSockets. They send chat messages and the server distributes them to all clients. We are going to bridge it with the chat system that has been escorting us throughout this tutorial, so that every message typed in Baeldung’s application (“the imported system” from now on) shows also on our system and vice versa.
We will achieve our goal by hooking into Baeldung’s code (that you can get here) and turning it into a Spiderwiz application that traps chat messages and imports them into the Spiderwiz network, as well as exports chat messages received over our network to the imported system. Since our system requires that chat messages are exchanged inside Chat Rooms and the imported system does not have this notion, we will configure a room name to it and users of our system will need to join that room in order to exchange messages with the imported system.
Baeldung’s project is called java-websocket
. We will stay with this project and update it. The
first step is to add the spiderwiz-core
dependency to its POM:
<dependency> <groupId>org.spiderwiz</groupId> <artifactId>spiderwiz-core</artifactId> <version>4.0</version> </dependency>
Note that spiderwiz-websocket-server
shall not be included because it would conflict with the mechanism already
provided by Baeldung.
We will place all our stuff in a new package – org.spiderwiz.tutorial.lesson17
. We
start as usual with WebsocketBridgeMain
:
package org.spiderwiz.tutorial.lesson17; import com.baeldung.model.Message; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.core.ImportHandler; import org.spiderwiz.core.Main; import org.spiderwiz.tutorial.objectLib.ChatApp; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; /** * Main class for the Websocket Bridge. */ public class WebsocketBridgeMain extends Main{ private static final String ROOT_DIRECTORY = "/tests/WebsocketBridge"; private static final String CONFIG_FILE_NAME = "ws-bridge.conf"; private static final String APP_NAME = "Websocket Chat Bridge"; private static final String APP_VERSION = "Z17.01"; // Version Z17.01: First working version private WebsocketImportHandler importHandler = null; public WebsocketBridgeMain() { super(ROOT_DIRECTORY, CONFIG_FILE_NAME, APP_NAME, APP_VERSION); } /** * @return the class instance as WebsocketBridgeMain type. */ public static WebsocketBridgeMain getInstance() { return (WebsocketBridgeMain)Main.getInstance(); } /** * @return the produced objects */ @Override protected String[] getProducedObjects() { return new String[]{ ChatRoom.ObjectCode, Chatter.ObjectCode, ChatMessage.ObjectCode }; } /** * @return the consumed objects */ @Override protected String[] getConsumedObjects() { return new String[]{ Chatter.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(ChatRoom.class); factoryList.add(ChatApp.class); factoryList.add(ChatterBridge.class); factoryList.add(ChatMessageBridge.class); } /** * @return the custom import handler instead of the default one. */ @Override protected ImportHandler createImportHandler() { return new WebsocketImportHandler(); } public synchronized WebsocketImportHandler getImportHandler() { return importHandler; } public synchronized void setImportHandler(WebsocketImportHandler importHandler) { this.importHandler = importHandler; } /** * Pass an imported message to the current import handler, if exists. * @param message */ public void processImportedMessage(Message message) { WebsocketImportHandler handler = getImportHandler(); if (handler != null) handler.processMessage(message); } }
The produced object types (line 39) are:
ChatRoom
: imports the configured room name into our system.Chatter
: tells when a user of the imported system connects or disconnects.ChatMessage
: imports chat messages.
The consumed object types (line 51) are:
Chatter
: informs the imported system when a chatter on our system joins or leaves the configured room.ChatMessage
: exports received messages to the imported system.
We register all implementation classes in populateObjectFactory() (line 63).
As mentioned, we need to extend ImportHandler
. We will do it with WebsocketImportHandler
that we need to register in createImportHandler() (line 75) instead of the
default handler.
The rest are methods that we use internally in our
implementation – getImportHandler()
(line 79) and setImportHandler()
(line 83) for getting and setting the importHandler
property and processImportedMessage()
(line 91) for processing an imported message. We will see their use in a
moment.
Now to the more interesting stuff. Here is WebsocketImportHandler
:
package org.spiderwiz.tutorial.lesson17; import com.baeldung.model.Message; import com.baeldung.websocket.ChatEndpoint; import java.util.Map; import org.spiderwiz.core.DataObject; import org.spiderwiz.core.ImportHandler; import org.spiderwiz.tutorial.objectLib.ChatRoom; /** * Implements a custom Import Handler for bridging with Baeldung's Websocket chat service. */ public class WebsocketImportHandler extends ImportHandler { private ChatRoom chatRoom = null; /** * Check if the configuration is for a bridge with Baeldung's Websocket chat service ("websocket" key exists) and if the * bridge has not been set yet, then configure it. The configuration shall include a "room" parameter that specifies the * chat room used by all users of the bridged service. * @param configParams a map of key=value configuration parameters. * @param n the <em>n</em> value of the <em>import-n</em> property. * @return true on success. */ @Override protected boolean configure(Map<String, String> configParams, int n) { if (!configParams.containsKey("websocket")) return false; if (WebsocketBridgeMain.getInstance().getImportHandler() != null) { WebsocketBridgeMain.getLogger().logEvent( "An import handler for Baeldung's Websocket chat service bridge has already been defined."); return false; } String room = configParams.get("room"); if (room == null) { WebsocketBridgeMain.getLogger().logEvent("No room name has been specified for Baeldung's Websocket chat service bridge"); return false; } WebsocketBridgeMain.getInstance().setImportHandler(this); try { // Create and commit a room object chatRoom = WebsocketBridgeMain.createTopLevelObject(ChatRoom.class, room); } catch (NoSuchFieldException | IllegalAccessException ex) { WebsocketBridgeMain.getLogger().logEvent("Room %1$s could not be created because of %2$s", room, ex.toString()); return false; } chatRoom.commit(); return true; } /** * Export an object by broadcasting it to all users of the Websocket chat service. * @param object the exported object, known to be of type com.baeldung.model.Message. * @return true */ @Override protected boolean exportObject(Object object) { ChatEndpoint.broadcastInternal((Message)object); return true; } /** * Identifies the handler by the name "websocket". * @return "websocket" */ @Override public String getName() { return "websocket"; } /** * At cleanup remove the associated ChatRoom object. */ @Override protected void cleanup() { if (chatRoom != null) { DataObject removed = chatRoom.remove(); if (removed != null) removed.commit(); } chatRoom = null; } /** * Process an imported message by calling the framework's processObject() method. * @param message */ public void processMessage(Message message) { try { processObject(message, null, message.getContent().length()); } catch (Exception ex) { WebsocketBridgeMain.getInstance().sendExceptionMail(ex, "While trying to import a message", null, false); } } /** * @return the associated chat room name. */ public String getRoomName() { return chatRoom == null ? null : chatRoom.getObjectID(); } /** * @return the associated ChatRoom object */ public ChatRoom getChatRoom() { return chatRoom; } }
The class extends ImportHandler. First we override configure() (line 25). The arguments to this
method are the parameters specified in the import-
n
property defined in the application’s configuration file. Our handler must have
the “websocket” keyword in its configuration and a “room” parameter that
defines the room name associated with the imported system. If any of these is
missing, or if a handler has already been defined (by another import-
n property), then the method
returns false
. If everything is OK
then we set the importHandler
property of WebsocketBridgeMain
to this
, then create and commit a ChatRoom
object to let our system know
about this room.
The exportObject() method, overridden in line 57,
accepts an object that is already converted to the exported format, in this
case a Message
object as defined in
Baeldung’s code. All it needs to do is to pass it over to the imported system
for distribution. This is done by calling the broadcastInternal()
method of the ChatEndpoint
class that we will see below.
The method getName() (line 67) returns a unique
identification for this handler, while cleanup() (line 75) removes the ChatRoom
object that is associated with
it.
The rest are methods that are used internally in our
implementation – processMessage()
(line 88) accepts a Message
object
from the imported system and pass it over for import processing by calling processObject(),
getRoomName()
(line 99)
returns the room name associated with this handler and getChatRoom()
(line 106) returns the ChatRoom
object associated with it.
Similarly to Lesson 11, we extend Chatter
and ChatMessage
to handle import-export. Here is ChatterBridge
:
package org.spiderwiz.tutorial.lesson17; import com.baeldung.model.Message; import org.spiderwiz.core.ImportHandler; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.zutils.ZDate; /** * Implementation class of Chatter for bridging with Baeldung's Websocket chat service. */ public class ChatterBridge extends Chatter { /** * Import the given data which is supposed to be a Message object. * If the content of the message is either "Connected!" or "Disconnected! then it is relevant to Chatter. * @param data the imported object. * @param channel the handler of the channel the data is imported from. Shall be "websocket". * @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 chatter name, or null * if the content is not "Connected!". * @throws Exception */ @Override protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception { // check if the import handler name is "websocket" if (!"websocket".equalsIgnoreCase(channel.getName())) return null; // switch by the message content Message message = (Message)data; switch(message.getContent()) { case "Disconnected!": // Import the removal then proceed as if connected. if (remove() == null) return null; case "Connected!": // Set user name and return the key list of the joining (or leaving) user setName(message.getFrom()); return new String[] { ((WebsocketImportHandler)channel).getRoomName(), getMyUUID(), getName() }; } // In all other cases return null return null; } /** * Export a "Connected!" or "Disconnected!" message * @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 exported message */ @Override protected Message exportObject(ImportHandler channel, String newID) { // Do not export my own object if (WebsocketBridgeMain.getInstance().getAppUUID().equals(getOriginUUID())) return null; // check if the import handler name is "websocket" if (!"websocket".equalsIgnoreCase(channel.getName())) return null; // check if the room name equals the name configured in the import handler. if (!getRoomName().equalsIgnoreCase(((WebsocketImportHandler)channel).getRoomName())) return null; // Generate a message with "Connected!" or a "Disconnected!" content Message message = new Message(); message.setFrom(getName()); message.setContent(newID == null ? "Connected!" : "Disconnected!"); return message; } /** * 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 WebsocketBridgeMain.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(); } }
If you followed the explanation in Lesson 11 then you should find this straightforward. We do exactly what we did there, except that instead of parsing serialized data we examine a Message
object. In importObject() (line 24) we check the content
property of the object. If it is “Disconnected!” then we call remove() on the object to mark it for deletion, then proceed as in the “Connected!” case. In both cases we import a Chatter
object by returning its key list. In any other case we return null
.
Likewise for exportObject() (line 57) we check if the room name equals the name configured for the imported system, and if it does then we return a Message
object with the content
property set to either “Connected!” or “Disconnected!” as necessary.
There is no change compared to Lesson 11 in the rest of the class.
The same concept applies to ChatMessageBridge
:
package org.spiderwiz.tutorial.lesson17; import com.baeldung.model.Message; 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; /** * Implementation class of ChatMessagee for bridging with Baeldung's Websocket chat service. */ 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; } /** * Import the given data which is supposed to be a Message object. * @param data the imported object. * @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 then chatter name * @throws Exception */ @Override protected String[] importObject(Object data, ImportHandler channel, ZDate ts) throws Exception { // check if the import handler name is "websocket" if (!"websocket".equalsIgnoreCase(channel.getName())) return null; // Process the message if it is neither "Connected!" nor "Disconnected!" Message message = (Message)data; switch(message.getContent()) { case "Connected!": case "Disconnected!": return null; } setMessage(message.getContent()); setTime(ts); // Return the key sequence return new String[] { ((WebsocketImportHandler)channel).getRoomName(), WebsocketBridgeMain.getInstance().getAppUUID().toString(), message.getFrom() }; } /** * Export this object as a Message object. * @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 exported Message object */ @Override protected Message exportObject(ImportHandler channel, String newID) { // Don't bounce imported objects if (WebsocketBridgeMain.getInstance().getAppUUID().equals(getOriginUUID())) return null; // check if the import handler name is "websocket" if (!"websocket".equalsIgnoreCase(channel.getName())) return null; // export String room = getParent().getParent().getParent().getObjectID(); if (!room.equalsIgnoreCase(((WebsocketImportHandler)channel).getRoomName())) return null; Message message = new Message(); message.setFrom(((Chatter)getParent()).getName()); message.setContent(getMessage()); return message; } /** * 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. WebsocketBridgeMain.getInstance().sendExceptionMail(ex, "Cannot instantiate ChatApp class", null, false); return false; } } }
Again, we do exactly what we did in Lesson 11, except that
instead of dealing with serialized data we deal with a Message
object.
We need to initialize the Spiderwiz engine at startup, so
our package includes RootServlet
:
package org.spiderwiz.tutorial.lesson17; 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 WebsocketBridgeMain().init(); } /** * Do Spiderwiz cleanup on application termination */ @Override public void destroy() { WebsocketBridgeMain.getInstance().cleanup(); super.destroy(); } }
This is identical to a class with the same name that we had
in Lesson
3 except that it instantiates and initializes WebsocketBridgeMain
.
It remains to zip our stuff with Baeldung’s code. We do it
by editing the latter’s ChatEndpoint
class:
package com.baeldung.websocket; import com.baeldung.model.Message; import java.io.IOException; import java.util.HashMap; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.EncodeException; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import org.spiderwiz.tutorial.lesson17.WebsocketBridgeMain; @ServerEndpoint(value = "/chat/{username}", decoders = MessageDecoder.class, encoders = MessageEncoder.class) public class ChatEndpoint { private Session session; private static final Set<ChatEndpoint> chatEndpoints = new CopyOnWriteArraySet<>(); private static final HashMap<String, String> users = new HashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("username") String username) throws IOException, EncodeException { this.session = session; chatEndpoints.add(this); users.put(session.getId(), username); Message message = new Message(); message.setFrom(username); message.setContent("Connected!"); broadcast(message); } @OnMessage public void onMessage(Session session, Message message) throws IOException, EncodeException { message.setFrom(users.get(session.getId())); broadcast(message); } @OnClose public void onClose(Session session) throws IOException, EncodeException { chatEndpoints.remove(this); Message message = new Message(); message.setFrom(users.get(session.getId())); message.setContent("Disconnected!"); broadcast(message); } @OnError public void onError(Session session, Throwable throwable) { // Do error handling here } /** * Broadcast a message both internally and over the bridge. * @param message */ private static void broadcast(Message message) { broadcastInternal(message); WebsocketBridgeMain.getInstance().processImportedMessage(message); } /** * Broadcast a message internally to all Websocket chat users. * @param message */ public static void broadcastInternal(Message message) { chatEndpoints.forEach(endpoint -> { synchronized (endpoint) { try { endpoint.session.getBasicRemote() .sendObject(message); } catch (IOException | EncodeException e) { // Use Spiderwiz framework to handle the exception WebsocketBridgeMain.getInstance().sendExceptionMail(e, "While sending a Message object", null, false); } } }); } }
We modified the broadcast()
method (line 61) by moving its code to broadcastInternal()
(line 70) and adding a call to WebsocketBridgeMain.processImportedMessage()
.
That means that every message is broadcast to both the internal users of the
imported system and to our system.
The method broadcastInternal()
that was just mentioned is declared public
so that we can call it from WebsocketImportHandler
when we need to export a message from our system to the imported system.
That’s all the coding work. For running and testing it we
need ws-bridge.conf
:
[log folder]Logs [producer-1]ip=localhost;port=10001 [import-1]websocket;room=Baeldung
To test it, deploy the project on your web server, on your web browser go to http://localhost/java-websocket/ (specify a port number if necessary), run one or more instances of our Chat Application, run also User Manager, login from our chat applications and join the Baeldung
room, connect users and type messages on any of the systems and see what happens.
We are close to the end of this tutorial. If you have gone with us all the way here then you deserve a personal benefit – Lesson 18 where you will learn how to customize SpiderAdmin.