Wiki source code of WebSocket Integration

Last modified by slauriere on 2026/01/28 15:01

Hide last authors
Marius Dumitru Florea 1.1 1 The following is a proposal on how to integrate / use the [[WebSocket>>https://en.wikipedia.org/wiki/WebSocket]] protocol in XWiki.
2
3 {{toc depth="3"/}}
4
5 = Use Cases =
6
7 * UC1: Be able to implement a WebSocket server end-point as an [[XWiki component>>extensions:Extension.Component Module]].
8 * UC2: Deploy component-based WebSocket end-points automatically when the XWiki web application starts.
9 * UC3: Allow XWiki extensions to provide WebSocket end-points that are deployed / undeployed automatically at runtime when the extension is installed / uninstalled.
10 * UC4: Be able to handle all WebSocket lifecycle events from the end-point component: connection open, close, error and message received
11 * UC5: Be able to send messages back (reply) from an end-point component
12 * UC6: Be able to install a WebSocket end-point in a sub-wiki (even if it requires programming rights)
13 * UC7: Be able to access the current user (authentication) from an end-point component
14 * UC8: Be able to access the current wiki from an end-point component
15 * UC9: Be able to access and modify XWiki documents from an end-point component
16
17 = WebSocket Protocol Implementations =
18
19 We basically have two options:
20
21 * rely on the implementation provided by the servlet container running XWiki; all servlet containers [[we support>>dev:Community.SupportStrategy.ServletContainerSupportStrategy.WebHome]] (Jetty and Tomcat) provide an implementation for the WebSocket protocol
Marius Dumitru Florea 8.1 22 * rely on a library (embedded WebSocket server); this is the path taken by the [[WebSocket Integration>>extensions:Extension.WebSocket]] extension from XWiki Contrib ([[##xwiki-contrib-websocket##>>https://github.com/xwiki-contrib/xwiki-contrib-websocket]]) which uses Netty.
Marius Dumitru Florea 1.1 23
24 For this proposal I chose the first option because it allows us to use a standard API, the [[Java API for WebSocket>>https://www.oracle.com/technical-resources/articles/java/jsr356.html]] (JSR356, with it's two versions 1.0 and 1.1). Note that there is another standard API, the [[Jakarta WebSocket 2.0>>https://jakarta.ee/specifications/websocket/2.0/]] that servlet containers implement in their latest versions, but we cannot use it ATM because we still need to support some older versions of these servlet containers. Netty doesn't implement the standard API so we would have to implement a bridge by ourselves.
25
26 = End-point Components =
27
28 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:
29
30 * 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
Marius Dumitru Florea 2.1 31 * 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
Marius Dumitru Florea 1.1 32
33 == Static End-points ==
34
Marius Dumitru Florea 6.1 35 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:
Marius Dumitru Florea 3.1 36
Marius Dumitru Florea 6.1 37 {{code language="none"}}
38 ws://<host>/<webAppContextPath>/websocket/<endPointPath>
39 ws://localhost:8080/xwiki/websocket/echo
40 {{/code}}
41
42 Here's a simple end-point implementation that simply echoes the messages it receives:
43
Marius Dumitru Florea 1.1 44 {{code language="java"}}
45 @Component
46 @Named("org.xwiki.websocket.internal.StaticEchoEndpoint")
47 @ServerEndpoint("/echo")
48 @Singleton
49 public class StaticEchoEndpoint implements EndpointComponent
50 {
51 @OnOpen
52 public void onOpen(Session session) throws IOException
53 {
54 session.getBasicRemote().sendText("Hi!");
55 }
56
57 @OnMessage
58 public String onMessage(Session session, String message)
59 {
60 return message;
61 }
62 }
63 {{/code}}
64
65 == Dynamic End-points ==
66
Marius Dumitru Florea 6.1 67 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:
Marius Dumitru Florea 3.1 68
Marius Dumitru Florea 6.1 69 {{code language="none"}}
70 ws://<host>/<webAppContextPath>/websocket/<wiki>/<endPointRoleHint>
71 ws://localhost:8080/xwiki/websocket/dev/echo
72 {{/code}}
73
74 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:
75
Marius Dumitru Florea 1.1 76 {{code language="java"}}
77 @Component
78 @Named("echo")
79 @Singleton
80 public class DynamicEchoEndpoint extends Endpoint implements EndpointComponent
81 {
82 @Override
83 public void onOpen(Session session, EndpointConfig config)
84 {
85 session.addMessageHandler(new MessageHandler.Whole<String>()
86 {
87 @Override
88 public void onMessage(String message)
89 {
90 DynamicEchoEndpoint.this.onMessage(session, message);
91 }
92 });
93 try {
94 session.getBasicRemote().sendText("Hi!");
95 } catch (IOException e) {
96 }
97 }
98
99 public void onMessage(Session session, String message)
100 {
101 try {
102 session.getBasicRemote().sendText(message);
103 } catch (IOException e) {
104 }
105 }
106 }
107 {{/code}}
108
Marius Dumitru Florea 6.1 109 Note that dynamic end-points must register message handlers explicitly in order to be able to handle received message.
110
Marius Dumitru Florea 1.1 111 == WebSocket Context ==
112
113 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.
114
115 {{code language="java"}}
116 @OnMessage
117 public String onMessage(Session session, String message) throws Exception
118 {
119 return this.context.call(session, () -> {
120 String currentWiki = this.modelContext.getCurrentEntityReference().extractReference(EntityType.WIKI).getName();
121 return String.format("[%s] %s -> %s", currentWiki, this.bridge.getCurrentUserReference(), message);
122 });
123 }
124 {{/code}}
125
126 == AbstractXWikiEndpoint ==
127
128 For dynamic end-points (those extending ##javax.websocket.Endpoint##) we propose to have an abstract base class to hold utility methods:
129
130 {{code language="java"}}
131 @Component
132 @Named("echo")
133 @Singleton
134 public class DynamicEchoEndpoint extends AbstractXWikiEndpoint
135 {
136 @Inject
137 private DocumentAccessBridge bridge;
138
139 @Inject
140 private ModelContext modelContext;
141
142 @Override
143 public void onOpen(Session session, EndpointConfig config)
144 {
145 this.context.run(session, () -> {
146 if (this.bridge.getCurrentUserReference() == null) {
147 close(session, CloseReason.CloseCodes.CANNOT_ACCEPT,
148 "We don't accept connections from guest users. Please login first.");
149 } else {
150 session.addMessageHandler(new MessageHandler.Whole<String>()
151 {
152 @Override
153 public void onMessage(String message)
154 {
155 handleMessage(session, message);
156 }
157 });
158 }
159 });
160 }
161
162 public String onMessage(String message)
163 {
164 String currentWiki = this.modelContext.getCurrentEntityReference().extractReference(EntityType.WIKI).getName();
165 return String.format("[%s] %s -> %s", currentWiki, this.bridge.getCurrentUserReference(), message);
166 }
167 }
168 {{/code}}
169
170 The base class will provide two methods for a start:
171
172 * ##close(Session, CloseCode, String)## to close the given session with the specified reason, handling errors
173 * ##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)
174
175 = WebSocket Script Service =
176
177 We propose to start with 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:
178
179 * if it starts with a slash then it represents a **path** so it will target the end-point mapped to that path:(((
180 {{code language="none"}}
181 $services.websocket.url('/echo')
182 ## ws://localhost:8080/xwiki/websocket/echo
183 {{/code}}
184 )))
185 * otherwise it represents a **role hint** so it will target the specified end-point component:(((
186 {{code language="none"}}
187 $services.websocket.url('echo')
188 ## ws://localhost:8080/xwiki/websocket/dev/echo
189 {{/code}}
190
191 The token before the end-point role hint is the wiki where to look for the component (the namespace).
192 )))
Marius Dumitru Florea 10.1 193
194 = Example =
195
196 Here's a JavaScript snippet that can be used to test the echo end-point:
197
198 {{code language="js"}}
199 require([], function() {
200 var ws = new WebSocket($jsontool.serialize($services.websocket.url('echo')));
201
202 ws.onopen = function() {
203 console.log("WebSocket opened.");
204 ws.send("Hello World!");
205 };
206
207 ws.onclose = function(event) {
208 console.log(`WebSocket closed: ${event.code} ${event.reason}`);
209 };
210
211 ws.onerror = function(event) {
212 console.log(`WebSocket error: ${event.code} ${event.reason}`);
213 };
214
215 var counter = 0;
216 ws.onmessage = function(message) {
217 console.log(message.data);
218 setTimeout(function() {
219 ws.send("Counter: " + counter++);
220 }, 5000);
221 };
222 });
223 {{/code}}

Get Connected