Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/asciidoc/websocket.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,81 @@ import io.jooby.jackson.Jackson2Module
}
----

==== Declarative definition

You can implement the same WebSocket as above using annotated classes in declarative style.
Ensure that `jooby-apt` is in the annotation processor path, annotate the class with javadoc:annotation.Path[],
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].

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.

.Java
[source,java,role="primary"]
----
@Path("/chat/{room}") // <1>
public class ChatSocket {

@OnConnect
public String onConnect(WebSocket ws, Context ctx) { // <2>
return "welcome";
}

@OnMessage
public Map<String, String> onMessage(WebSocket ws, Context ctx, WebSocketMessage message) { // <3>
return Map.of("echo", message.value());
// ws.send(message.value()); // <4>
}

@OnClose
public void onClose(WebSocket ws, Context ctx, WebSocketCloseStatus status) {}

@OnError
public void onError(WebSocket ws, Context ctx, Throwable cause) {}
}

// Application startup:
{
ws(new ChatSocketWs_()); // <5>
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
@Path("/chat/{room}") // <1>
class ChatSocket {

@OnConnect
fun onConnect(ws: WebSocket, ctx: Context): String { // <2>
return "welcome"
}

@OnMessage
fun onMessage(ws: WebSocket, ctx: Context, message: WebSocketMessage): Map<String, String> { // <3>
return mapOf("echo" to message.value())
// ws.send(message.value()) // <4>
}

@OnClose
fun onClose(ws: WebSocket, ctx: Context, status: WebSocketCloseStatus) {}

@OnError
fun onError(ws: WebSocket, ctx: Context, cause: Throwable) {}
}

// Application startup:
{
ws(ChatSocketWs_()) // <5>
}
----

<1> WebSocket route patterns for this handler.
<2> Returning a value sends it to the client without calling `send`.
<3> Return a value for automatic encoding (see <<Structured Data>>)
<4> You still can use `ws.send(...)` if method return type is `void`.
<5> Register the generated extension with javadoc:Jooby[ws, io.jooby.Extension].


==== Options

===== Connection Timeouts
Expand All @@ -192,3 +267,6 @@ websocket.maxSize = 128K
----

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].



10 changes: 10 additions & 0 deletions jooby/src/main/java/io/jooby/Jooby.java
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,16 @@ public Route.Set mvc(Extension router) {
}
}

/**
* Add websocket routes from a generated handler extension.
*
* @param router Websocket extension.
* @return Route set.
*/
public Route.Set ws(Extension router) {
return mvc(router);
}

@Override
public Route ws(String pattern, WebSocket.Initializer handler) {
return router.ws(pattern, handler);
Expand Down
22 changes: 22 additions & 0 deletions jooby/src/main/java/io/jooby/annotation/ws/OnClose.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.annotation.ws;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks method as WebSocket close callback.
*
* @author kliushnichenko
* @since 4.4.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClose {
}
21 changes: 21 additions & 0 deletions jooby/src/main/java/io/jooby/annotation/ws/OnConnect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.annotation.ws;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks method as WebSocket open callback.
*
* @author kliushnichenko
* @since 4.4.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnConnect {}
21 changes: 21 additions & 0 deletions jooby/src/main/java/io/jooby/annotation/ws/OnError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.annotation.ws;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks method as WebSocket error callback.
*
* @author kliushnichenko
* @since 4.4.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnError {}
21 changes: 21 additions & 0 deletions jooby/src/main/java/io/jooby/annotation/ws/OnMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.annotation.ws;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks method as WebSocket incoming message callback.
*
* @author kliushnichenko
* @since 4.4.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnMessage {}
12 changes: 12 additions & 0 deletions modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import io.jooby.internal.apt.*;

import io.jooby.internal.apt.ws.WsRouter;

/** Process jooby/jakarta annotation and generate source code from MVC controllers. */
@SupportedOptions({
DEBUG,
Expand Down Expand Up @@ -155,6 +157,11 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
if (!trpcRouter.isEmpty()) {
activeRouters.add(trpcRouter);
}

var wsRouter = WsRouter.parse(context, controller);
if (!wsRouter.isEmpty()) {
activeRouters.add(wsRouter);
}
}

verifyBeanValidationDependency(activeRouters);
Expand Down Expand Up @@ -276,6 +283,11 @@ public Set<String> getSupportedAnnotationTypes() {
supportedTypes.add("io.jooby.annotation.mcp.McpPrompt");
supportedTypes.add("io.jooby.annotation.mcp.McpResource");
supportedTypes.add("io.jooby.annotation.mcp.McpServer");
// Add WS Annotations
supportedTypes.add("io.jooby.annotation.ws.OnConnect");
supportedTypes.add("io.jooby.annotation.ws.OnClose");
supportedTypes.add("io.jooby.annotation.ws.OnMessage");
supportedTypes.add("io.jooby.annotation.ws.OnError");
return supportedTypes;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,12 @@ private Optional<String> mediaType(Function<Element, List<String>> lookup) {
.collect(Collectors.joining(", ", "java.util.List.of(", ")")));
}

private String javadocComment(boolean kt, String routerName) {
if (kt) {
return CodeBlock.statement("/** See [", routerName, ".", getMethodName(), "]", " */");
}
return CodeBlock.statement(
"/** See {@link ",
routerName,
"#",
getMethodName(),
"(",
String.join(", ", getRawParameterTypes(true, false)),
") */");
}

public List<String> generateMapping(boolean kt, String routerName, boolean isLastRoute) {
List<String> block = new ArrayList<>();
var methodName = getGeneratedName();
var returnType = getReturnType();
var paramString = String.join(", ", getJavaMethodSignature(kt));
var javadocLink = javadocComment(kt, routerName);
var javadocLink = seeControllerMethodJavadoc(kt, routerName);
var attributeGenerator = new RouteAttributesGenerator(context, hasBeanValidation);

var httpMethod =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public List<MvcParameter> getParameters(boolean skipCoroutine) {
.toList();
}

static String leadingSlash(String path) {
public static String leadingSlash(String path) {
if (path == null || path.isEmpty() || path.equals("/")) {
return "/";
}
Expand Down Expand Up @@ -124,6 +124,25 @@ public List<String> getRawParameterTypes(
.toList();
}

public String seeControllerMethodJavadoc(boolean kt, CharSequence controllerSimpleName) {
if (kt) {
return CodeBlock.statement(
"/** See [", controllerSimpleName, ".", getMethodName(), "]", " */");
}
return CodeBlock.statement(
"/** See {@link ",
controllerSimpleName,
"#",
getMethodName(),
"(",
String.join(", ", getRawParameterTypes(true, false)),
")} */");
}

public String seeControllerMethodJavadoc(boolean kt) {
return seeControllerMethodJavadoc(kt, getRouter().getTargetType().getSimpleName());
}

/**
* Returns the return type of the route method. Used to determine if the route returns a reactive
* type that requires static imports.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.apt.ws;

public enum WsLifecycle {
CONNECT,
MESSAGE,
CLOSE,
ERROR
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.apt.ws;

import java.util.EnumMap;
import java.util.Set;

final class WsParamTypes {

static final String RAW_WEBSOCKET = "io.jooby.WebSocket";
static final String RAW_CONTEXT = "io.jooby.Context";
static final String RAW_MESSAGE = "io.jooby.WebSocketMessage";
static final String RAW_CLOSE_STATUS = "io.jooby.WebSocketCloseStatus";
static final String RAW_THROWABLE = "java.lang.Throwable";

private static final EnumMap<WsLifecycle, Set<String>> ALLOWED_TYPES =
new EnumMap<>(WsLifecycle.class);

static {
ALLOWED_TYPES.put(WsLifecycle.CONNECT, Set.of(RAW_WEBSOCKET, RAW_CONTEXT));
ALLOWED_TYPES.put(
WsLifecycle.MESSAGE, Set.of(RAW_WEBSOCKET, RAW_CONTEXT, RAW_MESSAGE));
ALLOWED_TYPES.put(
WsLifecycle.CLOSE, Set.of(RAW_WEBSOCKET, RAW_CONTEXT, RAW_CLOSE_STATUS));
ALLOWED_TYPES.put(
WsLifecycle.ERROR, Set.of(RAW_WEBSOCKET, RAW_CONTEXT, RAW_THROWABLE));
}

static Set<String> getAllowedTypes(WsLifecycle lifecycle) {
return ALLOWED_TYPES.get(lifecycle);
}

static String generateArgumentName(String rawType) {
return switch (rawType) {
case RAW_WEBSOCKET -> "ws";
case RAW_CONTEXT -> "ctx";
case RAW_MESSAGE -> "message";
case RAW_CLOSE_STATUS -> "status";
case RAW_THROWABLE -> "cause";
default -> null;
};
}
}
Loading
Loading