WebSocket Integration
Adds support for writing WebSocket end-points as XWiki components. |
Type | JAR |
Category | API |
Developed by | |
Rating | |
License | GNU Lesser General Public License 2.1 |
Bundled With | XWiki Standard |
Compatibility | XWiki Standard 13.7RC1+ |
Table of contents
Description
This module adds support for writing WebSocket end-points as XWiki Components using the standard Java API for WebSocket (JSR 356). For deploying the end-points we rely on the servlet container, which means the servlet container needs to support WebSockets (i.e. it needs to provide an implementation of the JSR 356 specs). At the moment, this includes all the servlet containers we support.
End-point Components
All XWiki WebSocket end-point components will have to implement the org.xwiki.websocket.EndpointComponent role which is just a marker interface (no methods). In order to write the end-point component you will have to use the standard Java API for WebSocket. Based on how the end-points are deployed / registered we can split them in two categories:
- statically registered: these are implemented using the annotated WebSocket API (@ServerEndpoint); they are deployed only when the XWiki web application is started so they must be available at that time
- dynamically registered: these are implemented by extending javax.websocket.Endpoint from the standard API; they can be deployed at runtime, e.g. when an XWiki extension is installed
Static End-points
Static end-points have to specify the path they are mapped to. The path can be an URL template so it can accept path parameters. The final URL that will be used to access such an end-point will look like this:
ws://localhost:8080/xwiki/websocket/echo
Here's a simple end-point implementation that simply echoes the messages it receives:
@Named("org.xwiki.websocket.internal.StaticEchoEndpoint")
@ServerEndpoint("/echo")
@Singleton
public class StaticEchoEndpoint implements EndpointComponent
{
@OnOpen
public void onOpen(Session session) throws IOException
{
session.getBasicRemote().sendText("Hi!");
}
@OnMessage
public String onMessage(Session session, String message)
{
return message;
}
}
Dynamic End-points
Dynamic end-points can't specify the path they are mapped to. The path is determined automatically based on the role hint. The final URL that will be used to access such an end-point will look like this:
ws://localhost:8080/xwiki/websocket/dev/echo
The wiki is needed in the URL in order to look for the end-point component in the right namespace. Here's a simple end-point implementation that simply echoes the messages it receives:
@Named("echo")
@Singleton
public class DynamicEchoEndpoint extends Endpoint implements EndpointComponent
{
@Override
public void onOpen(Session session, EndpointConfig config)
{
session.addMessageHandler(new MessageHandler.Whole<String>()
{
@Override
public void onMessage(String message)
{
DynamicEchoEndpoint.this.onMessage(session, message);
}
});
try {
session.getBasicRemote().sendText("Hi!");
} catch (IOException e) {
}
}
public void onMessage(Session session, String message)
{
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
}
}
}
Note that dynamic end-points must register message handlers explicitly in order to be able to handle received message.
WebSocket Context
If your end-point depends on the XWiki context (current wiki, current user, etc.) then you can inject the WebSocketContext component and use it to run your code with the XWiki context properly initialized.
protected WebSocketContext context;
@Inject
private ModelContext modelContext;
@Inject
private DocumentAccessBridge bridge;
@OnMessage
public String onMessage(Session session, String message) throws Exception
{
return this.context.call(session, () -> {
String currentWiki = this.modelContext.getCurrentEntityReference().extractReference(EntityType.WIKI).getName();
return String.format("[%s] %s -> %s", currentWiki, this.bridge.getCurrentUserReference(), message);
});
}
AbstractXWikiEndpoint
For dynamic end-points (those extending javax.websocket.Endpoint) there is an abstract base class you can extend in order to have access to some utility methods:
@Named("echo")
@Singleton
public class DynamicEchoEndpoint extends AbstractXWikiEndpoint
{
@Inject
private DocumentAccessBridge bridge;
@Inject
private ModelContext modelContext;
@Override
public void onOpen(Session session, EndpointConfig config)
{
this.context.run(session, () -> {
if (this.bridge.getCurrentUserReference() == null) {
close(session, CloseReason.CloseCodes.CANNOT_ACCEPT,
"We don't accept connections from guest users. Please login first.");
} else {
session.addMessageHandler(new MessageHandler.Whole<String>()
{
@Override
public void onMessage(String message)
{
handleMessage(session, message);
}
});
}
});
}
public String onMessage(String message)
{
String currentWiki = this.modelContext.getCurrentEntityReference().extractReference(EntityType.WIKI).getName();
return String.format("[%s] %s -> %s", currentWiki, this.bridge.getCurrentUserReference(), message);
}
}
The base class provides two methods for now:
- close(Session, CloseCode, String) to close the given session with the specified reason, handling errors
- handleMessage(Session, T) to handle a received message by calling the onMessage method and sending back the value returned by it (similar to the way the @OnMessage annotation behaves)
WebSocket Script Service
The script service has a single $services.websocket.url(String) method to obtain the URL needed to connect to and communicate with the WebSocket end-point. The string parameter will be used to identify the WebSocket end-point this way:
- if it starts with a slash then it represents a path so it will target the end-point mapped to that path:$services.websocket.url('/echo')
## ws://localhost:8080/xwiki/websocket/echo - otherwise it represents a role hint so it will target the specified end-point component:$services.websocket.url('echo')
## ws://localhost:8080/xwiki/websocket/dev/echoIn case you are wondering, the token before the end-point role hint is the wiki where to look for the component (the namespace).
Example
Here's a JavaScript snippet that can be used to test the echo end-point:
var ws = new WebSocket($jsontool.serialize($services.websocket.url('echo')));
ws.onopen = function() {
console.log("WebSocket opened.");
ws.send("Hello World!");
};
ws.onclose = function(event) {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
};
ws.onerror = function(event) {
console.log(`WebSocket error: ${event.code} ${event.reason}`);
};
var counter = 0;
ws.onmessage = function(message) {
console.log(message.data);
setTimeout(function() {
ws.send("Counter: " + counter++);
}, 5000);
};
});
Prerequisites & Installation Instructions
We recommend using the Extension Manager to install this extension (Make sure that the text "Installable with the Extension Manager" is displayed at the top right location on this page to know if this extension can be installed with the Extension Manager).
You can also use the manual method which involves dropping the JAR file and all its dependencies into the WEB-INF/lib folder and restarting XWiki.
Dependencies
Dependencies for this extension (org.xwiki.platform:xwiki-platform-websocket 16.9.0):
- org.xwiki.commons:xwiki-commons-websocket 16.9.0
- org.xwiki.platform:xwiki-platform-oldcore 16.9.0