Skip to content

Commit 515e319

Browse files
declarative websockets
1 parent a17c769 commit 515e319

17 files changed

Lines changed: 693 additions & 16 deletions

File tree

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 {}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 class as Websocket handler.
15+
*
16+
* <p>Register the generated {@link io.jooby.Extension} with {@link io.jooby.Jooby#ws(io.jooby.Extension)}.
17+
*
18+
* <pre>{@code
19+
* @WebSocketRoute("/chat/{username}")
20+
* public class ChatWebsocket {
21+
*
22+
* @OnMessage
23+
* public String onMessage(WebSocketMessage message) { ... }
24+
*
25+
* }
26+
* }</pre>
27+
*
28+
* @author kliushnichenko
29+
* @since 4.4.1
30+
*/
31+
@Retention(RetentionPolicy.RUNTIME)
32+
@Target(ElementType.TYPE)
33+
public @interface WebSocketRoute {
34+
/**
35+
* WebSocket route patterns (Ant-style), same rules as {@link io.jooby.annotation.Path}.
36+
*
37+
* @return Patterns.
38+
*/
39+
String[] value() default {};
40+
}

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

Lines changed: 13 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,12 @@ 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.WebSocketRoute");
288+
supportedTypes.add("io.jooby.annotation.ws.OnConnect");
289+
supportedTypes.add("io.jooby.annotation.ws.OnClose");
290+
supportedTypes.add("io.jooby.annotation.ws.OnMessage");
291+
supportedTypes.add("io.jooby.annotation.ws.OnError");
279292
return supportedTypes;
280293
}
281294

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.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.internal.apt.ws;
7+
8+
import static io.jooby.internal.apt.CodeBlock.semicolon;
9+
import static io.jooby.internal.apt.CodeBlock.statement;
10+
import static java.lang.System.lineSeparator;
11+
12+
import java.util.StringJoiner;
13+
14+
import javax.lang.model.element.ExecutableElement;
15+
16+
import io.jooby.internal.apt.CodeBlock;
17+
import io.jooby.internal.apt.MvcParameter;
18+
import io.jooby.internal.apt.TypeDefinition;
19+
import io.jooby.internal.apt.WebRoute;
20+
21+
public class WsHandlerMethod extends WebRoute<WsRouter> {
22+
23+
public WsHandlerMethod(WsRouter router, ExecutableElement method) {
24+
super(router, method);
25+
}
26+
27+
@Override
28+
public boolean hasBeanValidation() {
29+
return false;
30+
}
31+
32+
@Override
33+
public TypeDefinition getReturnType() {
34+
var types = context.getProcessingEnvironment().getTypeUtils();
35+
return new TypeDefinition(types, method.getReturnType());
36+
}
37+
38+
public void appendBody(boolean kt, StringBuilder buffer, String indent) {
39+
buffer.append(
40+
statement(indent, CodeBlock.var(kt), "c = this.factory.apply(ctx)", semicolon(kt)));
41+
42+
TypeDefinition wsReturnType = getReturnType();
43+
var expr = invocation(kt);
44+
45+
if (isUncheckedCast()) {
46+
buffer
47+
.append(indent)
48+
.append(
49+
kt ? "@Suppress(\"UNCHECKED_CAST\") " : "@SuppressWarnings(\"unchecked\") ")
50+
.append(lineSeparator());
51+
}
52+
53+
if (wsReturnType.isVoid()) {
54+
buffer.append(statement(indent, expr, semicolon(kt)));
55+
return;
56+
}
57+
58+
buffer.append(
59+
statement(
60+
indent, kt ? "val" : "var", " __wsReturn = ", expr, semicolon(kt)));
61+
String rawErasure = wsReturnType.getRawType().toString();
62+
switch (rawErasure) {
63+
case "java.lang.String", "byte[]", "java.nio.ByteBuffer" ->
64+
buffer.append(statement(indent, "ws.send(__wsReturn)", semicolon(kt)));
65+
default ->
66+
buffer.append(statement(indent, "ws.render(__wsReturn)", semicolon(kt)));
67+
}
68+
}
69+
70+
public String invocation(boolean kt) {
71+
return makeCall(kt, paramList(), false, false);
72+
}
73+
74+
private String paramList() {
75+
var joiner = new StringJoiner(", ", "(", ")");
76+
for (var param : getParameters(true)) {
77+
joiner.add(wsParameterName(param));
78+
}
79+
return joiner.toString();
80+
}
81+
82+
private String wsParameterName(MvcParameter parameter) {
83+
String rawParamType = parameter.getType().getRawType().toString();
84+
var name = WsParamTypes.generateArgumentName(rawParamType);
85+
if (name != null) {
86+
return name;
87+
}
88+
89+
getContext()
90+
.error("Unsupported websocket handler parameter type: %s.", rawParamType);
91+
return "null";
92+
}
93+
}

0 commit comments

Comments
 (0)