From d78eee563665cf4dd9bb9992214de71b049e5b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Pupier?= Date: Wed, 30 Dec 2020 17:00:26 +0100 Subject: [PATCH] Provide support for Jakarta Websocket #471 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - will require an update of lsp4j.target.target file when orbit bundle will be available, see https://git.eclipse.org/r/c/orbit/orbit-recipes/+/174146 - to avoid API break, created a second bundle with the Jakarta Websocket. it will allow also to support old version too for a moment. the drawback is that it is making the naming a bit more complicated. Signed-off-by: Aurélien Pupier --- CHANGELOG.md | 3 +- gradle/versions.gradle | 1 + org.eclipse.lsp4j.websocket.jakarta/.project | 27 ++ .../build.gradle | 24 ++ .../websocket/jakarta/WebSocketEndpoint.java | 51 +++ .../jakarta/WebSocketLauncherBuilder.java | 76 ++++ .../jakarta/WebSocketMessageConsumer.java | 72 ++++ .../jakarta/WebSocketMessageHandler.java | 47 +++ .../jakarta/test/MockConnectionTest.java | 170 +++++++++ .../jakarta/test/MockEndpointConfig.java | 39 ++ .../websocket/jakarta/test/MockSession.java | 346 ++++++++++++++++++ releng/lsp4j-feature/feature.xml | 13 + releng/pom.xml | 2 +- releng/releng-target/lsp4j.target.target | 2 + settings.gradle | 1 + 15 files changed, 872 insertions(+), 2 deletions(-) create mode 100644 org.eclipse.lsp4j.websocket.jakarta/.project create mode 100644 org.eclipse.lsp4j.websocket.jakarta/build.gradle create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketEndpoint.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketLauncherBuilder.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageConsumer.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/main/java/org/eclipse/lsp4j/websocket/jakarta/WebSocketMessageHandler.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockConnectionTest.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockEndpointConfig.java create mode 100644 org.eclipse.lsp4j.websocket.jakarta/src/test/java/org/eclipse/lsp4j/websocket/jakarta/test/MockSession.java 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 02485119..3cf542b9 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -19,5 +19,6 @@ ext.versions = [ 'gson': '2.8.6', 'gson_orbit': '2.8.6.v20201231-1626', '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/pom.xml b/releng/pom.xml index 28bad926..eac710a7 100644 --- a/releng/pom.xml +++ b/releng/pom.xml @@ -95,7 +95,7 @@ org.eclipse.lsp4j - org.eclipse.lsp4j.websocket + org.eclipse.lsp4j.websocket.jakarta 0.11.0-SNAPSHOT sources diff --git a/releng/releng-target/lsp4j.target.target b/releng/releng-target/lsp4j.target.target index f2ef6a4e..8f14ff53 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'