WebSocket Integration

Last modified by Admin on 2024/04/30 16:01

server_connectAdds support for writing WebSocket end-points as XWiki components.
TypeJAR
CategoryAPI
Developed by

XWiki Development Team

Rating
0 Votes
LicenseGNU Lesser General Public License 2.1
Bundled With

XWiki Standard

Compatibility

XWiki Standard 13.7RC1+

Installable with the Extension Manager

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://<host>/<webAppContextPath>/websocket/<endPointPath>
ws://localhost:8080/xwiki/websocket/echo

Here's a simple end-point implementation that simply echoes the messages it receives:

@Component
@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://<host>/<webAppContextPath>/websocket/<wiki>/<endPointRoleHint>
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:

@Component
@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.

@Inject
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:

@Component
@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/echo

    In 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:

require([], function() {
 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.3.0):

  • org.xwiki.commons:xwiki-commons-websocket 16.3.0
  • org.xwiki.platform:xwiki-platform-oldcore 16.3.0

Get Connected