Skip to content

Commit e3a6d7c

Browse files
authored
Merge pull request #3917 from kliushnichenko/feat/declarative-websockets
declarative websockets
2 parents a17c769 + a3dbdf5 commit e3a6d7c

20 files changed

Lines changed: 863 additions & 16 deletions

File tree

docs/asciidoc/websocket.adoc

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,122 @@ import io.jooby.jackson.Jackson2Module
168168
}
169169
----
170170

171+
==== Declarative definition
172+
173+
You can implement the same WebSocket as above using annotated classes in declarative style.
174+
Ensure that `jooby-apt` is in the annotation processor path, annotate the class with javadoc:annotation.Path[],
175+
and mark methods with javadoc:annotation.ws.OnConnect[], javadoc:annotation.ws.OnMessage[], javadoc:annotation.ws.OnClose[], and javadoc:annotation.ws.OnError[]. Compile code to generate an extension javadoc:Extension[] and register it by calling javadoc:Jooby[ws, io.jooby.Extension].
176+
177+
When a lifecycle method **returns** a value, that value is written to the client automatically: plain text or binary for `String`, `byte[]`, and `ByteBuffer`, and structured values (for example JSON) using the same encoders as in <<Structured Data>>. Alternatively, use a **void** method and send with `ws.send(...)` on the javadoc:WebSocket[] argument.
178+
179+
.Java
180+
[source,java,role="primary"]
181+
----
182+
@Path("/chat/{room}") // <1>
183+
public class ChatSocket {
184+
185+
@OnConnect
186+
public String onConnect(WebSocket ws, Context ctx) { // <2>
187+
return "welcome";
188+
}
189+
190+
@OnMessage
191+
public Map<String, String> onMessage(WebSocket ws, Context ctx, WebSocketMessage message) { // <3>
192+
return Map.of("echo", message.value());
193+
// ws.send(message.value()); // <4>
194+
}
195+
196+
@OnClose
197+
public void onClose(WebSocket ws, Context ctx, WebSocketCloseStatus status) {}
198+
199+
@OnError
200+
public void onError(WebSocket ws, Context ctx, Throwable cause) {}
201+
}
202+
203+
// Application startup:
204+
{
205+
ws(new ChatSocketWs_()); // <5>
206+
}
207+
----
208+
209+
.Kotlin
210+
[source,kotlin,role="secondary"]
211+
----
212+
@Path("/chat/{room}") // <1>
213+
class ChatSocket {
214+
215+
@OnConnect
216+
fun onConnect(ws: WebSocket, ctx: Context): String { // <2>
217+
return "welcome"
218+
}
219+
220+
@OnMessage
221+
fun onMessage(ws: WebSocket, ctx: Context, message: WebSocketMessage): Map<String, String> { // <3>
222+
return mapOf("echo" to message.value())
223+
// ws.send(message.value()) // <4>
224+
}
225+
226+
@OnClose
227+
fun onClose(ws: WebSocket, ctx: Context, status: WebSocketCloseStatus) {}
228+
229+
@OnError
230+
fun onError(ws: WebSocket, ctx: Context, cause: Throwable) {}
231+
}
232+
233+
// Application startup:
234+
{
235+
ws(ChatSocketWs_()) // <5>
236+
}
237+
----
238+
239+
<1> WebSocket route patterns for this handler.
240+
<2> Returning a value sends it to the client without calling `send`.
241+
<3> Return a value for automatic encoding (see <<Structured Data>>)
242+
<4> You still can use `ws.send(...)` if method return type is `void`.
243+
<5> Register the generated extension with javadoc:Jooby[ws, io.jooby.Extension].
244+
245+
`@OnMessage` handlers also support parsing messages into structured data, similar to MVC methods:
246+
247+
.Java
248+
[source,java,role="primary"]
249+
----
250+
@Path("/chat/{room}")
251+
public class ChatSocket {
252+
253+
record ChatMessage(String username, String message, String type) {}
254+
255+
@OnMessage
256+
public Map<String, ChatMessage> onMessage(ChatMessage message) { // <1>
257+
return Map.of("echo", message);
258+
}
259+
260+
...
261+
}
262+
----
263+
264+
.Kotlin
265+
[source,kotlin,role="secondary"]
266+
----
267+
@Path("/chat/{room}")
268+
class ChatSocket {
269+
270+
data class ChatMessage(
271+
val username: String,
272+
val message: String,
273+
val type: String
274+
)
275+
276+
@OnMessage
277+
fun onMessage(message: ChatMessage): Map<String, ChatMessage> { // <1>
278+
return mapOf("echo" to message)
279+
}
280+
281+
...
282+
}
283+
284+
----
285+
<1> WebSocket message is automatically decoded into `ChatMessage` structure.
286+
171287
==== Options
172288

173289
===== Connection Timeouts
@@ -192,3 +308,6 @@ websocket.maxSize = 128K
192308
----
193309

194310
See the Typesafe Config documentation for the supported https://github.com/lightbend/config/blob/master/HOCON.md#size-in-bytes-format[size in bytes format].
311+
312+
313+

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,16 @@ public Route.Set mvc(Extension router) {
520520
}
521521
}
522522

523+
/**
524+
* Add websocket routes from a generated handler extension.
525+
*
526+
* @param router Websocket extension.
527+
* @return Route set.
528+
*/
529+
public Route.Set ws(Extension router) {
530+
return mvc(router);
531+
}
532+
523533
@Override
524534
public Route ws(String pattern, WebSocket.Initializer handler) {
525535
return router.ws(pattern, handler);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.annotation.ws;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks method as WebSocket close callback.
15+
*
16+
* @author kliushnichenko
17+
* @since 4.4.1
18+
*/
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Target(ElementType.METHOD)
21+
public @interface OnClose {
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.annotation.ws;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks method as WebSocket open callback.
15+
*
16+
* @author kliushnichenko
17+
* @since 4.4.1
18+
*/
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Target(ElementType.METHOD)
21+
public @interface OnConnect {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.annotation.ws;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks method as WebSocket error callback.
15+
*
16+
* @author kliushnichenko
17+
* @since 4.4.1
18+
*/
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Target(ElementType.METHOD)
21+
public @interface OnError {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.annotation.ws;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks method as WebSocket incoming message callback.
15+
*
16+
* @author kliushnichenko
17+
* @since 4.4.1
18+
*/
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Target(ElementType.METHOD)
21+
public @interface OnMessage {}

modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import io.jooby.internal.apt.*;
2727

28+
import io.jooby.internal.apt.ws.WsRouter;
29+
2830
/** Process jooby/jakarta annotation and generate source code from MVC controllers. */
2931
@SupportedOptions({
3032
DEBUG,
@@ -155,6 +157,11 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
155157
if (!trpcRouter.isEmpty()) {
156158
activeRouters.add(trpcRouter);
157159
}
160+
161+
var wsRouter = WsRouter.parse(context, controller);
162+
if (!wsRouter.isEmpty()) {
163+
activeRouters.add(wsRouter);
164+
}
158165
}
159166

160167
verifyBeanValidationDependency(activeRouters);
@@ -276,6 +283,11 @@ public Set<String> getSupportedAnnotationTypes() {
276283
supportedTypes.add("io.jooby.annotation.mcp.McpPrompt");
277284
supportedTypes.add("io.jooby.annotation.mcp.McpResource");
278285
supportedTypes.add("io.jooby.annotation.mcp.McpServer");
286+
// Add WS Annotations
287+
supportedTypes.add("io.jooby.annotation.ws.OnConnect");
288+
supportedTypes.add("io.jooby.annotation.ws.OnClose");
289+
supportedTypes.add("io.jooby.annotation.ws.OnMessage");
290+
supportedTypes.add("io.jooby.annotation.ws.OnError");
279291
return supportedTypes;
280292
}
281293

modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,8 @@ private List<? extends AnnotationMirror> annotationFromAnnotationType(Element el
183183
public boolean isRequireBeanValidation() {
184184
return requireBeanValidation;
185185
}
186+
187+
public VariableElement variableElement() {
188+
return parameter;
189+
}
186190
}

modules/jooby-apt/src/main/java/io/jooby/internal/apt/RestRoute.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,12 @@ private Optional<String> mediaType(Function<Element, List<String>> lookup) {
8282
.collect(Collectors.joining(", ", "java.util.List.of(", ")")));
8383
}
8484

85-
private String javadocComment(boolean kt, String routerName) {
86-
if (kt) {
87-
return CodeBlock.statement("/** See [", routerName, ".", getMethodName(), "]", " */");
88-
}
89-
return CodeBlock.statement(
90-
"/** See {@link ",
91-
routerName,
92-
"#",
93-
getMethodName(),
94-
"(",
95-
String.join(", ", getRawParameterTypes(true, false)),
96-
") */");
97-
}
98-
9985
public List<String> generateMapping(boolean kt, String routerName, boolean isLastRoute) {
10086
List<String> block = new ArrayList<>();
10187
var methodName = getGeneratedName();
10288
var returnType = getReturnType();
10389
var paramString = String.join(", ", getJavaMethodSignature(kt));
104-
var javadocLink = javadocComment(kt, routerName);
90+
var javadocLink = seeControllerMethodJavadoc(kt, routerName);
10591
var attributeGenerator = new RouteAttributesGenerator(context, hasBeanValidation);
10692

10793
var httpMethod =

modules/jooby-apt/src/main/java/io/jooby/internal/apt/WebRoute.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public List<MvcParameter> getParameters(boolean skipCoroutine) {
7373
.toList();
7474
}
7575

76-
static String leadingSlash(String path) {
76+
public static String leadingSlash(String path) {
7777
if (path == null || path.isEmpty() || path.equals("/")) {
7878
return "/";
7979
}
@@ -124,6 +124,25 @@ public List<String> getRawParameterTypes(
124124
.toList();
125125
}
126126

127+
public String seeControllerMethodJavadoc(boolean kt, CharSequence controllerSimpleName) {
128+
if (kt) {
129+
return CodeBlock.statement(
130+
"/** See [", controllerSimpleName, ".", getMethodName(), "]", " */");
131+
}
132+
return CodeBlock.statement(
133+
"/** See {@link ",
134+
controllerSimpleName,
135+
"#",
136+
getMethodName(),
137+
"(",
138+
String.join(", ", getRawParameterTypes(true, false)),
139+
")} */");
140+
}
141+
142+
public String seeControllerMethodJavadoc(boolean kt) {
143+
return seeControllerMethodJavadoc(kt, getRouter().getTargetType().getSimpleName());
144+
}
145+
127146
/**
128147
* Returns the return type of the route method. Used to determine if the route returns a reactive
129148
* type that requires static imports.

0 commit comments

Comments
 (0)