Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
19 changes: 19 additions & 0 deletions avaje-jex-websockets/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex-parent</artifactId>
<version>3.3-RC4</version>
</parent>
<artifactId>avaje-jex-websockets</artifactId> <dependencies>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.avaje.jex.websocket;

import io.avaje.jex.http.Context;
import io.avaje.jex.websocket.WsContext.WsBinaryMessage;
import io.avaje.jex.websocket.WsContext.WsClose;
import io.avaje.jex.websocket.WsContext.WsError;
import io.avaje.jex.websocket.WsContext.WsMessage;
import io.avaje.jex.websocket.WsContext.WsOpen;
import io.avaje.jex.websocket.WsContext.WsPong;
import io.avaje.jex.websocket.exception.CloseCode;
import io.avaje.jex.websocket.internal.AbstractWebSocket;

class JexWebSocket extends AbstractWebSocket {

private final WebSocketListener listener;
private final Context ctx;

JexWebSocket(Context ctx, WebSocketListener listener) {
super(ctx.exchange());
this.listener = listener;
this.ctx = ctx;
}

@Override
protected void onClose(CloseCode code, String reason, boolean initiatedByRemote) {
listener.onClose(new WsClose(ctx, this, code, reason, initiatedByRemote));
}

@Override
protected void onMessage(WebSocketFrame frame) {
switch (frame.opCode()) {
case TEXT -> listener.onMessage(new WsMessage(ctx, this, frame, frame.textPayload()));
case BINARY ->
listener.onBinaryMessage(new WsBinaryMessage(ctx, this, frame, frame.binaryPayload()));
default -> throw new IllegalArgumentException("Unexpected value: ");
}
}

@Override
protected void onPong(WebSocketFrame pong) {
listener.onPong(new WsPong(ctx, this, pong));
}

@Override
protected void onOpen() {
listener.onOpen(new WsOpen(ctx, this));
}

@Override
protected void onError(Exception exception) {
listener.onError(new WsError(ctx, this, exception));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.avaje.jex.websocket;

import io.avaje.jex.websocket.exception.CloseCode;

/**
* Represents a WebSocket connection, providing methods to interact with the connection.
* Allows sending and receiving messages, pinging, and closing the connection.
*/
public interface WebSocket {

/**
* Checks if the WebSocket connection is open.
*
* @return true if the connection is open, false otherwise
*/
boolean isOpen();

/**
* Closes the WebSocket connection with the specified close code and reason.
*
* @param code the close code indicating the reason for closure
* @param reason the reason for closing the connection
* @param initiatedByRemote true if the close was initiated by the remote endpoint, false otherwise
*/
void close(CloseCode code, String reason, boolean initiatedByRemote);

/**
* Sends a ping frame with the specified payload to the remote endpoint.
*
* @param payload the ping payload as a byte array
*/
void ping(byte[] payload);

/**
* Sends a binary message to the remote endpoint.
*
* @param payload the binary payload as a byte array
*/
void send(byte[] payload);

/**
* Sends a text message to the remote endpoint.
*
* @param payload the text payload as a string
*/
void send(String payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.avaje.jex.websocket;

import io.avaje.jex.http.Context;
import io.avaje.jex.websocket.internal.WebSocketHandler;

class WebSocketExchangeHandler extends WebSocketHandler {

private final WebSocketListener listener;

WebSocketExchangeHandler(WebSocketListener listener) {
this.listener = listener;
}

@Override
protected JexWebSocket openWebSocket(Context exchange) {

return new JexWebSocket(exchange, listener);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.avaje.jex.websocket;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Represents a WebSocket frame as defined by RFC 6455. Provides access to frame payload, masking,
* opcode, and frame control information.
*/
public interface WebSocketFrame {

/**
* Returns the binary payload of the frame.
*
* @return the binary payload as a byte array
*/
byte[] binaryPayload();

/**
* Returns the masking key used for the frame, if present.
*
* @return the masking key as a byte array, or null if not masked
*/
byte[] maskingKey();

/**
* Returns the opcode of the frame.
*
* @return the opcode
*/
OpCode opCode();

/**
* Returns the text payload of the frame, if applicable.
*
* @return the text payload, or null if not a text frame
*/
String textPayload();

/**
* Indicates if this frame is the final fragment in a message.
*
* @return true if final fragment, false otherwise
*/
boolean isFin();

/**
* Indicates if the frame is masked.
*
* @return true if masked, false otherwise
*/
boolean isMasked();

/**
* Writes the frame to the given output stream in WebSocket frame format.
*
* @param out the output stream to write to
* @throws IOException if an I/O error occurs
*/
void write(OutputStream out) throws IOException;

/** WebSocket opcodes */
public enum OpCode {
CONTINUATION(0),
TEXT(1),
BINARY(2),
CLOSE(8),
PING(9),
PONG(10);

private final byte code;
private static final Map<Byte, OpCode> VALUES =
Arrays.stream(values()).collect(Collectors.toMap(OpCode::value, e -> e));

OpCode(int code) {
this.code = (byte) code;
}

/**
* Finds the OpCode corresponding to the given byte value.
*
* @param value the opcode value
* @return the matching OpCode, or null if not found
*/
public static OpCode find(byte value) {
return VALUES.get(value);
}

/**
* Returns the byte value of this opcode.
*
* @return the opcode value
*/
public byte value() {
return this.code;
}

/**
* Indicates if this opcode is a control frame (close, ping, pong).
*
* @return true if control frame, false otherwise
*/
public boolean isControlFrame() {
return this == CLOSE || this == PING || this == PONG;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.avaje.jex.websocket;

import io.avaje.jex.websocket.WsContext.WsBinaryMessage;
import io.avaje.jex.websocket.WsContext.WsClose;
import io.avaje.jex.websocket.WsContext.WsError;
import io.avaje.jex.websocket.WsContext.WsMessage;
import io.avaje.jex.websocket.WsContext.WsOpen;
import io.avaje.jex.websocket.WsContext.WsPong;

/**
* Holds the different WebSocket handlers for a specific {@link WsHandlerEntry} or the WebSocket
* log.
*/
public interface WebSocketListener {
/**
* Called when a binary message is received.
*
* @param binaryPayload the binary payload
*/
default void onBinaryMessage(WsBinaryMessage binaryPayload) {}

/**
* Called when the websocket is closed.
*
* @param wsClose the close context
*/
default void onClose(WsClose wsClose) {}

/**
* Called when a text message is received.
*
* @param message the text message
*/
default void onMessage(WsMessage message) {}

/**
* Called when the websocket is opened.
*
* @param wsOpenContext the open context
*/
default void onOpen(WsOpen wsOpen) {}

/**
* Called when a pong is received.
*
* @param pong the pong
*/
default void onPong(WsPong wsPong) {}

/**
* Called when an error occurs.
*
* @param wsError the error
*/
default void onError(WsError wsError) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.avaje.jex.websocket;

import java.util.ArrayList;
import java.util.List;

import io.avaje.jex.Jex;
import io.avaje.jex.Routing;
import io.avaje.jex.security.Role;
import io.avaje.jex.spi.JexPlugin;

public class WebSocketPlugin implements JexPlugin {

private final List<Routing.HttpService> handlers = new ArrayList<>();

public WebSocketPlugin ws(String path, WebSocketListener listener, Role... roles) {
handlers.add(r -> r.get(path, new WebSocketExchangeHandler(listener), roles));
return this;
}

@Override
public void apply(Jex jex) {
jex.routing().addAll(handlers);
}

public static WebSocketPlugin create() {
return new WebSocketPlugin();
}
}
Loading
Loading