diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5a7a362..9c2c8f0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,9 @@
### v0.11.0 (Date TBD)
-Fixed issues: https://github.com/eclipse/lsp4j/milestone/18?closed=1
+ * Added new module `org.eclipse.lsp4j.websocket.jakarta` for using LSP4J over Jakarta websockets
+Fixed issues: https://github.com/eclipse/lsp4j/milestone/18?closed=1
### v0.10.0 (Nov. 2020)
diff --git a/gradle/versions.gradle b/gradle/versions.gradle
index 2bf33ea8..e204369b 100644
--- a/gradle/versions.gradle
+++ b/gradle/versions.gradle
@@ -19,5 +19,6 @@ ext.versions = [
'gson': '2.8.2',
'gson_orbit': '2.8.2.v20180104-1110',
'websocket': '1.0',
+ 'websocket_jakarta': '2.0.0',
'junit': '4.12'
]
diff --git a/org.eclipse.lsp4j.websocket.jakarta/.project b/org.eclipse.lsp4j.websocket.jakarta/.project
new file mode 100644
index 00000000..7a52be5f
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/.project
@@ -0,0 +1,27 @@
+
+
+ org.eclipse.lsp4j.websocket.jakarta
+ WebSocket support for LSP4J
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.xtext.ui.shared.xtextNature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+ org.eclipse.xtext.ui.shared.xtextBuilder
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+
diff --git a/org.eclipse.lsp4j.websocket.jakarta/build.gradle b/org.eclipse.lsp4j.websocket.jakarta/build.gradle
new file mode 100644
index 00000000..8e2f1d0d
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/build.gradle
@@ -0,0 +1,24 @@
+/******************************************************************************
+ * Copyright (c) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+
+ext.title = 'LSP4J WebSocket Jakarta'
+description = 'Jakarta WebSocket support for LSP4J'
+
+dependencies {
+ compile project(":org.eclipse.lsp4j.jsonrpc")
+ compile "jakarta.websocket:jakarta.websocket-api:$versions.websocket_jakarta"
+ testCompile "junit:junit:$versions.junit"
+}
+
+jar.manifest {
+ instruction 'Import-Package', '*'
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketEndpoint.java b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketEndpoint.java
new file mode 100644
index 00000000..2f8c338c
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketEndpoint.java
@@ -0,0 +1,51 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta;
+
+import java.util.Collection;
+
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.Session;
+
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+
+/**
+ * WebSocket endpoint implementation that connects to a JSON-RPC service.
+ *
+ * @param remote service interface type
+ */
+public abstract class WebSocketEndpoint extends Endpoint {
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ WebSocketLauncherBuilder builder = new WebSocketLauncherBuilder<>();
+ builder.setSession(session);
+ configure(builder);
+ Launcher launcher = builder.create();
+ connect(builder.getLocalServices(), launcher.getRemoteProxy());
+ }
+
+ /**
+ * Configure the JSON-RPC launcher. Implementations should set at least the
+ * {@link Launcher.Builder#setLocalService(Object) local service} and the
+ * {@link Launcher.Builder#setRemoteInterface(Class) remote interface}.
+ */
+ protected abstract void configure(Launcher.Builder builder);
+
+ /**
+ * Override this in order to connect the local services to the remote service proxy.
+ */
+ protected void connect(Collection localServices, T remoteProxy) {
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketLauncherBuilder.java b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketLauncherBuilder.java
new file mode 100644
index 00000000..f46adf5b
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketLauncherBuilder.java
@@ -0,0 +1,76 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta;
+
+import java.util.Collection;
+
+import jakarta.websocket.Session;
+
+import org.eclipse.lsp4j.jsonrpc.Endpoint;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
+import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint;
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.eclipse.lsp4j.jsonrpc.services.ServiceEndpoints;
+
+/**
+ * JSON-RPC launcher builder for use in {@link WebSocketEndpoint}.
+ *
+ * @param remote service interface type
+ */
+public class WebSocketLauncherBuilder extends Launcher.Builder {
+
+ protected Session session;
+
+ public Collection getLocalServices() {
+ return localServices;
+ }
+
+ public WebSocketLauncherBuilder setSession(Session session) {
+ this.session = session;
+ return this;
+ }
+
+ @Override
+ public Launcher create() {
+ if (localServices == null)
+ throw new IllegalStateException("Local service must be configured.");
+ if (remoteInterfaces == null)
+ throw new IllegalStateException("Remote interface must be configured.");
+
+ MessageJsonHandler jsonHandler = createJsonHandler();
+ RemoteEndpoint remoteEndpoint = createRemoteEndpoint(jsonHandler);
+ addMessageHandlers(jsonHandler, remoteEndpoint);
+ T remoteProxy = createProxy(remoteEndpoint);
+ return createLauncher(null, remoteProxy, remoteEndpoint, null);
+ }
+
+ @Override
+ protected RemoteEndpoint createRemoteEndpoint(MessageJsonHandler jsonHandler) {
+ MessageConsumer outgoingMessageStream = new WebSocketMessageConsumer(session, jsonHandler);
+ outgoingMessageStream = wrapMessageConsumer(outgoingMessageStream);
+ Endpoint localEndpoint = ServiceEndpoints.toEndpoint(localServices);
+ RemoteEndpoint remoteEndpoint;
+ if (exceptionHandler == null)
+ remoteEndpoint = new RemoteEndpoint(outgoingMessageStream, localEndpoint);
+ else
+ remoteEndpoint = new RemoteEndpoint(outgoingMessageStream, localEndpoint, exceptionHandler);
+ jsonHandler.setMethodProvider(remoteEndpoint);
+ return remoteEndpoint;
+ }
+
+ protected void addMessageHandlers(MessageJsonHandler jsonHandler, RemoteEndpoint remoteEndpoint) {
+ MessageConsumer messageConsumer = wrapMessageConsumer(remoteEndpoint);
+ session.addMessageHandler(new WebSocketMessageHandler(messageConsumer, jsonHandler, remoteEndpoint));
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageConsumer.java b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageConsumer.java
new file mode 100644
index 00000000..c1c28d6a
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageConsumer.java
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.websocket.Session;
+
+import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
+import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.eclipse.lsp4j.jsonrpc.messages.Message;
+
+/**
+ * Message consumer that sends messages via a WebSocket session.
+ */
+public class WebSocketMessageConsumer implements MessageConsumer {
+
+ private static final Logger LOG = Logger.getLogger(WebSocketMessageConsumer.class.getName());
+
+ private final Session session;
+ private final MessageJsonHandler jsonHandler;
+
+ public WebSocketMessageConsumer(Session session, MessageJsonHandler jsonHandler) {
+ this.session = session;
+ this.jsonHandler = jsonHandler;
+ }
+
+ public Session getSession() {
+ return session;
+ }
+
+ @Override
+ public void consume(Message message) {
+ String content = jsonHandler.serialize(message);
+ try {
+ sendMessage(content);
+ } catch (IOException exception) {
+ throw new JsonRpcException(exception);
+ }
+ }
+
+ protected void sendMessage(String message) throws IOException {
+ if (session.isOpen()) {
+ int length = message.length();
+ if (length <= session.getMaxTextMessageBufferSize()) {
+ session.getAsyncRemote().sendText(message);
+ } else {
+ int currentOffset = 0;
+ while (currentOffset < length) {
+ int currentEnd = Math.min(currentOffset + session.getMaxTextMessageBufferSize(), length);
+ session.getBasicRemote().sendText(message.substring(currentOffset, currentEnd), currentEnd == length);
+ currentOffset = currentEnd;
+ }
+ }
+ } else {
+ LOG.log(Level.INFO, "Ignoring message due to closed session: {0}", message);
+ }
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageHandler.java b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageHandler.java
new file mode 100644
index 00000000..15c32b54
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageHandler.java
@@ -0,0 +1,47 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta;
+
+import jakarta.websocket.MessageHandler;
+
+import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
+import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
+import org.eclipse.lsp4j.jsonrpc.MessageIssueHandler;
+import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
+import org.eclipse.lsp4j.jsonrpc.messages.Message;
+
+/**
+ * WebSocket message handler that parses JSON messages and forwards them to a {@link MessageConsumer}.
+ */
+public class WebSocketMessageHandler implements MessageHandler.Whole {
+
+ private final MessageConsumer callback;
+ private final MessageJsonHandler jsonHandler;
+ private final MessageIssueHandler issueHandler;
+
+ public WebSocketMessageHandler(MessageConsumer callback, MessageJsonHandler jsonHandler, MessageIssueHandler issueHandler) {
+ this.callback = callback;
+ this.jsonHandler = jsonHandler;
+ this.issueHandler = issueHandler;
+ }
+
+ public void onMessage(String content) {
+ try {
+ Message message = jsonHandler.parseMessage(content);
+ callback.consume(message);
+ } catch (MessageIssueException exception) {
+ // An issue was found while parsing or validating the message
+ issueHandler.handle(exception.getRpcMessage(), exception.getIssues());
+ }
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockConnectionTest.java b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockConnectionTest.java
new file mode 100644
index 00000000..6ba15bd4
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockConnectionTest.java
@@ -0,0 +1,170 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta.test;
+
+import java.util.Collection;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
+import org.eclipse.lsp4j.websocket.jakarta.WebSocketEndpoint;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockConnectionTest {
+
+ private static final long TIMEOUT = 2000;
+
+ private Client client;
+ private Server server;
+
+ @SuppressWarnings("resource")
+ @Before
+ public void setup() {
+ client = new Client();
+ server = new Server();
+ MockSession clientSession = new MockSession();
+ MockSession serverSession = new MockSession();
+ clientSession.connect(serverSession);
+ clientSession.open(new ClientSideEndpoint());
+ serverSession.open(new ServerSideEndpoint());
+ }
+
+ @Test
+ public void testClientRequest() throws Exception {
+ CompletableFuture future = client.server.request("foo");
+ String result = future.get(TIMEOUT, TimeUnit.MILLISECONDS);
+ Assert.assertEquals("foobar", result);
+ }
+
+ @Test
+ public void testNotifications() throws Exception {
+ server.client.notify("12");
+ await(() -> client.result.length() == 2);
+ client.server.notify("foo");
+ await(() -> server.result.length() == 3);
+ server.client.notify("34");
+ await(() -> client.result.length() == 4);
+ client.server.notify("bar");
+ await(() -> server.result.length() == 6);
+ server.client.notify("56");
+ await(() -> client.result.length() == 6);
+
+ Assert.assertEquals("foobar", server.result);
+ Assert.assertEquals("123456", client.result);
+ }
+
+ @Test
+ public void testChunkedNotification() throws Exception {
+ StringBuilder messageBuilder = new StringBuilder();
+ Random random = new Random(1);
+ for (int i = 0; i < 3 * MockSession.MAX_CHUNK_SIZE; i++) {
+ messageBuilder.append((char) ('a' + random.nextInt('z' - 'a' + 1)));
+ }
+ String message = messageBuilder.toString();
+
+ server.client.notify(message);
+ await(() -> client.result.length() == message.length());
+
+ Assert.assertEquals(message, client.result);
+ }
+
+ private void await(Supplier condition) throws InterruptedException {
+ long startTime = System.currentTimeMillis();
+ while (!condition.get()) {
+ Thread.sleep(20);
+ if (System.currentTimeMillis() - startTime > TIMEOUT) {
+ Assert.fail("Timeout elapsed while waiting for condition.\n");
+ }
+ }
+ }
+
+ private static interface ClientInterface {
+
+ @JsonNotification("client/notify")
+ void notify(String arg);
+
+ }
+
+ private static class Client implements ClientInterface {
+ ServerInterface server;
+ String result = "";
+
+ @Override
+ public void notify(String arg) {
+ this.result += arg;
+ }
+ }
+
+ private static interface ServerInterface {
+
+ @JsonRequest("server/request")
+ CompletableFuture request(String arg);
+
+ @JsonNotification("server/notify")
+ void notify(String arg);
+
+ }
+
+ private static class Server implements ServerInterface {
+ ClientInterface client;
+ String result = "";
+
+ @Override
+ public CompletableFuture request(String arg) {
+ return CompletableFuture.supplyAsync(() -> arg + "bar");
+ }
+
+ @Override
+ public void notify(String arg) {
+ this.result += arg;
+ }
+ }
+
+ private class ClientSideEndpoint extends WebSocketEndpoint {
+
+ @Override
+ protected void configure(Launcher.Builder builder) {
+ builder
+ .setLocalService(client)
+ .setRemoteInterface(ServerInterface.class);
+ }
+
+ @Override
+ protected void connect(Collection localServices, ServerInterface remoteProxy) {
+ localServices.forEach(s -> ((Client) s).server = remoteProxy);
+ }
+
+ }
+
+ private class ServerSideEndpoint extends WebSocketEndpoint {
+
+ @Override
+ protected void configure(Launcher.Builder builder) {
+ builder
+ .setLocalService(server)
+ .setRemoteInterface(ClientInterface.class);
+ }
+
+ @Override
+ protected void connect(Collection localServices, ClientInterface remoteProxy) {
+ localServices.forEach(s -> ((Server) s).client = remoteProxy);
+ }
+
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockEndpointConfig.java b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockEndpointConfig.java
new file mode 100644
index 00000000..78f3ee22
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockEndpointConfig.java
@@ -0,0 +1,39 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta.test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.websocket.Decoder;
+import jakarta.websocket.Encoder;
+import jakarta.websocket.EndpointConfig;
+
+public class MockEndpointConfig implements EndpointConfig {
+
+ @Override
+ public List> getEncoders() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List> getDecoders() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Map getUserProperties() {
+ return Collections.emptyMap();
+ }
+
+}
diff --git a/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockSession.java b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockSession.java
new file mode 100644
index 00000000..e3381224
--- /dev/null
+++ b/org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockSession.java
@@ -0,0 +1,346 @@
+/******************************************************************************
+ * Copyright (c) 2021 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.websocket.jakarta.test;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.EncodeException;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.Extension;
+import jakarta.websocket.MessageHandler;
+import jakarta.websocket.MessageHandler.Partial;
+import jakarta.websocket.MessageHandler.Whole;
+import jakarta.websocket.RemoteEndpoint;
+import jakarta.websocket.SendHandler;
+import jakarta.websocket.SendResult;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+
+public class MockSession implements Session {
+
+ public static final int MAX_CHUNK_SIZE = 100;
+
+ private final BasicRemote basicRemote = new BasicRemote();
+ private final AsyncRemote asyncRemote = new AsyncRemote();
+ private final Set messageHandlers = new HashSet<>();
+ private Endpoint endpoint;
+ private MockSession connectedSession;
+ private boolean isClosed;
+ private StringBuilder partialMessage;
+
+ public void connect(MockSession other) {
+ this.connectedSession = other;
+ other.connectedSession = this;
+ }
+
+ @Override
+ public RemoteEndpoint.Async getAsyncRemote() {
+ return asyncRemote;
+ }
+
+ @Override
+ public RemoteEndpoint.Basic getBasicRemote() {
+ return basicRemote;
+ }
+
+ public void open(Endpoint endpoint) {
+ this.endpoint = endpoint;
+ endpoint.onOpen(this, new MockEndpointConfig());
+ }
+
+ @Override
+ public void close() throws IOException {
+ close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "OK"));
+ }
+
+ @Override
+ public void close(CloseReason closeReason) throws IOException {
+ isClosed = true;
+ endpoint.onClose(this, closeReason);
+ }
+
+ @Override
+ public void addMessageHandler(MessageHandler handler) throws IllegalStateException {
+ if (!messageHandlers.add(handler))
+ throw new IllegalStateException();
+ }
+
+
+ @Override
+ public void addMessageHandler(Class clazz, Whole handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addMessageHandler(Class clazz, Partial handler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set getMessageHandlers() {
+ return messageHandlers;
+ }
+
+ @Override
+ public void removeMessageHandler(MessageHandler handler) {
+ messageHandlers.remove(handler);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void dispatch(String message, boolean lastChunk) {
+ if (lastChunk) {
+ String wholeMessage = message;
+ if (partialMessage != null) {
+ partialMessage.append(message);
+ wholeMessage = partialMessage.toString();
+ partialMessage = null;
+ }
+ for (MessageHandler h : connectedSession.messageHandlers) {
+ if (h instanceof MessageHandler.Whole>)
+ ((MessageHandler.Whole) h).onMessage(wholeMessage);
+ else
+ ((MessageHandler.Partial) h).onMessage(message, true);
+ };
+ } else {
+ if (partialMessage == null) {
+ partialMessage = new StringBuilder();
+ }
+ for (MessageHandler h : connectedSession.messageHandlers) {
+ if (h instanceof MessageHandler.Partial>)
+ ((MessageHandler.Partial) h).onMessage(message, false);
+ };
+ partialMessage.append(message);
+ }
+ }
+
+ @Override
+ public WebSocketContainer getContainer() {
+ return null;
+ }
+
+ @Override
+ public String getProtocolVersion() {
+ return "13";
+ }
+
+ @Override
+ public String getNegotiatedSubprotocol() {
+ return null;
+ }
+
+ @Override
+ public List getNegotiatedExtensions() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return true;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return !isClosed;
+ }
+
+ @Override
+ public long getMaxIdleTimeout() {
+ return 10000;
+ }
+
+ @Override
+ public void setMaxIdleTimeout(long milliseconds) {
+ }
+
+ @Override
+ public void setMaxBinaryMessageBufferSize(int length) {
+ }
+
+ @Override
+ public int getMaxBinaryMessageBufferSize() {
+ return 100;
+ }
+
+ @Override
+ public void setMaxTextMessageBufferSize(int length) {
+ }
+
+ @Override
+ public int getMaxTextMessageBufferSize() {
+ return MAX_CHUNK_SIZE;
+ }
+
+ @Override
+ public String getId() {
+ return "mock";
+ }
+
+ @Override
+ public URI getRequestURI() {
+ try {
+ return new URI("http://localhost:8080/mock");
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Map> getRequestParameterMap() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public String getQueryString() {
+ return "";
+ }
+
+ @Override
+ public Map getPathParameters() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Map getUserProperties() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return null;
+ }
+
+ @Override
+ public Set getOpenSessions() {
+ return Collections.singleton(this);
+ }
+
+ private class BasicRemote extends AbstractRemoteEndpoint implements RemoteEndpoint.Basic {
+
+ @Override
+ public void sendText(String text) throws IOException {
+ dispatch(text, true);
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer data) throws IOException {
+ }
+
+ @Override
+ public void sendText(String partialMessage, boolean isLast) throws IOException {
+ dispatch(partialMessage, isLast);
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException {
+ }
+
+ @Override
+ public OutputStream getSendStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public Writer getSendWriter() throws IOException {
+ return null;
+ }
+
+ @Override
+ public void sendObject(Object data) throws IOException, EncodeException {
+ }
+
+ }
+
+ private class AsyncRemote extends AbstractRemoteEndpoint implements RemoteEndpoint.Async {
+
+ @Override
+ public long getSendTimeout() {
+ return 1000;
+ }
+
+ @Override
+ public void setSendTimeout(long timeoutmillis) {
+ }
+
+ @Override
+ public void sendText(String text, SendHandler handler) {
+ sendText(text).thenRun(() -> {
+ handler.onResult(new SendResult());
+ });
+ }
+
+ @Override
+ public CompletableFuture sendText(String text) {
+ return CompletableFuture.runAsync(() -> {
+ dispatch(text, true);
+ });
+ }
+
+ @Override
+ public Future sendBinary(ByteBuffer data) {
+ return null;
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer data, SendHandler handler) {
+ }
+
+ @Override
+ public Future sendObject(Object data) {
+ return null;
+ }
+
+ @Override
+ public void sendObject(Object data, SendHandler handler) {
+ }
+
+ }
+
+ private static abstract class AbstractRemoteEndpoint implements RemoteEndpoint {
+
+ @Override
+ public void setBatchingAllowed(boolean allowed) throws IOException {
+ }
+
+ @Override
+ public boolean getBatchingAllowed() {
+ return false;
+ }
+
+ @Override
+ public void flushBatch() throws IOException {
+ }
+
+ @Override
+ public void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException {
+ }
+
+ @Override
+ public void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException {
+ }
+
+ }
+
+}
diff --git a/releng/lsp4j-feature/feature.xml b/releng/lsp4j-feature/feature.xml
index 59ca9fc9..197464ce 100644
--- a/releng/lsp4j-feature/feature.xml
+++ b/releng/lsp4j-feature/feature.xml
@@ -114,4 +114,17 @@ SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
version="0.0.0"
unpack="false"/>
+
+
+
diff --git a/releng/releng-target/lsp4j.target.target b/releng/releng-target/lsp4j.target.target
index 40a75d53..dd2de8fa 100644
--- a/releng/releng-target/lsp4j.target.target
+++ b/releng/releng-target/lsp4j.target.target
@@ -10,6 +10,8 @@
+
+
diff --git a/settings.gradle b/settings.gradle
index df9bea2d..748d49a2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,3 +17,4 @@ include 'org.eclipse.lsp4j.generator'
include 'org.eclipse.lsp4j.jsonrpc'
include 'org.eclipse.lsp4j.jsonrpc.debug'
include 'org.eclipse.lsp4j.websocket'
+include 'org.eclipse.lsp4j.websocket.jakarta'