From f0ac2dc0fded6740518174ce1752f4d56af04abd Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 8 Oct 2024 23:07:08 +1100 Subject: [PATCH 1/7] Dev UI: Allow custom page links in the side menu Signed-off-by: Phillip Kruger --- .../resources/dev-ui/qwc/qwc-extensions.js | 19 +++- .../src/main/resources/dev-ui/qwc/qwc-menu.js | 98 +++++++++++++++++-- 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js index a9c0b73a17493..d555580dcaf02 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js @@ -42,9 +42,13 @@ export class QwcExtensions extends observeState(LitElement) { flex-flow: column wrap; padding-top: 5px; } - .float-right { + .float-right { align-self: flex-end; } + + qwc-extension-link { + cursor: grab; + } `; static properties = { @@ -199,11 +203,22 @@ export class QwcExtensions extends observeState(LitElement) { ?embed=${page.embed} externalUrl="${page.metadata.externalUrl}" dynamicUrlMethodName="${page.metadata.dynamicUrlMethodName}" - webcomponent="${page.componentLink}" > + webcomponent="${page.componentLink}" + draggable="true" @dragstart="${this._handleDragStart}"> `)}`; } + _handleDragStart(event) { + const extensionNamespace = event.currentTarget.getAttribute('namespace'); + const pageId = event.currentTarget.getAttribute('path'); + + const extension = devuiState.cards.active.find(obj => obj.namespace === extensionNamespace); + const page = extension.cardPages.find(obj => obj.id === pageId); + const jsonData = JSON.stringify(page); + event.dataTransfer.setData('application/json', jsonData); + } + _renderInactive(extension){ if(extension.unlisted === "false"){ return html` - `; } + _handleDragOver(event) { + event.preventDefault(); + } + + _handleDrop(event) { + event.preventDefault(); + + const data = event.dataTransfer.getData('application/json'); + const customMenu = JSON.parse(data); + + let storedMenu = this._restoreDynamicMenuItems(); + + const index = storedMenu.findIndex(obj => obj.id === customMenu.id); + if (index === -1) { + storedMenu.push(customMenu); + } + + this._storeDynamicMenuItems(storedMenu); + this._dynamicMenuNamespaces = this._restoreDynamicMenuItems(); + } + + _restoreDynamicMenuItems(){ + let menu = this.storageControl.get('customPageLinks'); + if(menu){ + return JSON.parse(menu); + }else{ + return []; + } + } + + _storeDynamicMenuItems(menu){ + this.storageControl.set('customPageLinks', JSON.stringify(menu)); + } + _renderVersion(){ if(this._show){ return html`
@@ -165,12 +206,45 @@ export class QwcMenu extends observeState(LitElement) { } } + _renderCustomItem(page, index){ + if(page){ + const index = devuiState.cards.active.findIndex(obj => obj.namespace === page.namespace); // Only show if that extension is added + if (index !== -1) { + + let extensionName = ""; + if(page.metadata && page.metadata.extensionName){ + extensionName = page.metadata.extensionName; + } + + let items = [{ text: 'Remove', action: 'remove', id: page.id , namespace: page.namespace}]; + return html` + ${this._renderItem(page, index)} + `; + } + } + } + + _handleContextMenu(event){ + const selectedItem = event.detail.value; + if (selectedItem && selectedItem.action === 'remove') { + let storedMenu = this._restoreDynamicMenuItems(); + const index = storedMenu.findIndex(obj => obj.id === selectedItem.id); + if (index !== -1) { + storedMenu.splice(index, 1); + } + this._storeDynamicMenuItems(storedMenu); + this._dynamicMenuNamespaces = this._restoreDynamicMenuItems(); + } + } + _renderItem(page, index){ var defaultSelection = false; if(index===0)defaultSelection = true; - import(page.componentRef); - this.routerController.addRouteForMenu(page, defaultSelection); + if(page.componentRef){ + import(page.componentRef); + this.routerController.addRouteForMenu(page, defaultSelection); + } // Each namespace has one place on the menu if(!this._customMenuNamespaces.includes(page.namespace)){ @@ -183,16 +257,16 @@ export class QwcMenu extends observeState(LitElement) { displayName = page.title; } } - + let pageRef = this.routerController.getPageUrlFor(page); let classnames = this._getClassNamesForMenuItem(page, index); return html` - - - ${displayName} - - `; + + + ${displayName} + + `; } } @@ -230,7 +304,11 @@ export class QwcMenu extends observeState(LitElement) { } _renderIcon(icon, action){ - if((action == "smaller" && this._show) || (action == "larger" && !this._show)){ + if(action == "smaller" && this._show){ + return html` + + `; + }else if(action == "larger" && !this._show){ return html` `; From 4140a1716f1c7bcac417c67bc4aad315d6b6b556 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 8 Oct 2024 23:16:22 +1100 Subject: [PATCH 2/7] Dev UI: Remove information from menu Signed-off-by: Phillip Kruger --- .../info/deployment/InfoDevUIProcessor.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java index c33392ff490a1..47a8e6d6bc25b 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java @@ -5,7 +5,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; -import io.quarkus.devui.spi.page.MenuPageBuildItem; +import io.quarkus.devui.spi.page.ExternalPageBuilder; import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.WebComponentPageBuilder; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; @@ -17,8 +17,7 @@ public class InfoDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) - void create(BuildProducer menuPageProducer, - BuildProducer cardPageProducer, + void create(BuildProducer cardPageProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, InfoBuildTimeConfig config, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, @@ -27,25 +26,20 @@ void create(BuildProducer menuPageProducer, var path = nonApplicationRootPathBuildItem.resolveManagementPath(config.path(), managementInterfaceBuildTimeConfig, launchModeBuildItem); - MenuPageBuildItem menuBuildItem = new MenuPageBuildItem(); - - menuBuildItem.addBuildTimeData("infoUrl", path); - WebComponentPageBuilder infoPage = Page.webComponentPageBuilder() .title("Information") .icon("font-awesome-solid:circle-info") .componentLink("qwc-info.js"); - menuBuildItem.addPage(infoPage); - menuBuildItem.addPage(Page.externalPageBuilder("Raw") + ExternalPageBuilder rawPage = Page.externalPageBuilder("Raw") .url(path) .icon("font-awesome-solid:circle-info") - .isJsonContent()); - - menuPageProducer.produce(menuBuildItem); + .isJsonContent(); CardPageBuildItem cardBuildItem = new CardPageBuildItem(); + cardBuildItem.addBuildTimeData("infoUrl", path); cardBuildItem.addPage(infoPage); + cardBuildItem.addPage(rawPage); cardPageProducer.produce(cardBuildItem); } From a400e75e43643fd84d5e776f87b786eb8e55b1dd Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 8 Oct 2024 23:20:47 +1100 Subject: [PATCH 3/7] Dev UI: Change some extension card links order Signed-off-by: Phillip Kruger --- .../devui/SmallRyeGraphQLDevUIProcessor.java | 2 +- .../deployment/devui/OpenApiDevUIProcessor.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java index 495a617a5cf23..7b5c516eb9335 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java @@ -35,8 +35,8 @@ CardPageBuildItem createCard(NonApplicationRootPathBuildItem nonApplicationRootP .doNotEmbed() .url("https://graphql.org/"); - cardPageBuildItem.addPage(schemaPage); cardPageBuildItem.addPage(uiPage); + cardPageBuildItem.addPage(schemaPage); cardPageBuildItem.addPage(learnLink); return cardPageBuildItem; diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java index 52602cfb894dc..5e690fc1b1e9c 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java @@ -27,6 +27,11 @@ public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRoo CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); + cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI") + .url(uiPath + "/index.html?embed=true", uiPath) + .isHtmlContent() + .icon("font-awesome-solid:signs-post")); + cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") .url(schemaPath, schemaPath) .isYamlContent() @@ -38,11 +43,6 @@ public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRoo .isJsonContent() .icon("font-awesome-solid:file-code")); - cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI") - .url(uiPath + "/index.html?embed=true", uiPath) - .isHtmlContent() - .icon("font-awesome-solid:signs-post")); - return cardPageBuildItem; } From aab9355f340f8ea3a98e9679a960cdb266a48b3e Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Thu, 10 Oct 2024 10:35:07 +0300 Subject: [PATCH 4/7] Register JBoss EnhancedQueueExecutor$RuntimeFields for runtime init As suggested in the source code https://github.com/jbossas/jboss-threads/blob/60a6d35ec56e85f683ff45d89b29ea4f7b0e01aa/src/main/java/org/jboss/threads/EnhancedQueueExecutor.java#L325 Closes https://github.com/quarkusio/quarkus/issues/43797 --- .../quarkus/deployment/JBossThreadsProcessor.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/JBossThreadsProcessor.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/JBossThreadsProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/JBossThreadsProcessor.java new file mode 100644 index 0000000000000..dc45d2f0b305b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/JBossThreadsProcessor.java @@ -0,0 +1,14 @@ +package io.quarkus.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; + +public class JBossThreadsProcessor { + + @BuildStep + RuntimeInitializedClassBuildItem build() { + // TODO: Remove once we move to a jboss-threads version that handles this in its native-image.properties file + // see https://github.com/jbossas/jboss-threads/pull/200 + return new RuntimeInitializedClassBuildItem("org.jboss.threads.EnhancedQueueExecutor$RuntimeFields"); + } +} From a3d3a327284588017462f83feab74ede8841fbfe Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 9 Oct 2024 17:46:37 +0200 Subject: [PATCH 5/7] WebSockets Next: make it possible to store user data in a connection - resolves #43772 --- .../asciidoc/websockets-next-reference.adoc | 69 ++++++++++++ .../connection/ConnectionUserDataTest.java | 92 ++++++++++++++++ .../quarkus/websockets/next/Connection.java | 103 ++++++++++++++++++ .../io/quarkus/websockets/next/UserData.java | 59 ++++++++++ .../next/WebSocketClientConnection.java | 80 +------------- .../websockets/next/WebSocketConnection.java | 89 +-------------- .../websockets/next/runtime/UserDataImpl.java | 44 ++++++++ .../next/runtime/WebSocketConnectionBase.java | 27 ++++- 8 files changed, 396 insertions(+), 167 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/connection/ConnectionUserDataTest.java create mode 100644 extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/Connection.java create mode 100644 extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UserData.java create mode 100644 extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/UserDataImpl.java diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index e36ea297344b4..36d7a9b98f3f2 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -640,6 +640,40 @@ class MyBean { There are also other convenient methods. For example, `OpenConnections#findByEndpointId(String)` makes it easy to find connections for a specific endpoint. +==== User data + +It is also possible to associate arbitrary user data with a specific connection. +The `io.quarkus.websockets.next.UserData` object obtained by the `WebSocketConnection#userData()` method represents mutable user data associated with a connection. + +[source, java] +---- +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.UserData.TypedKey; + +@WebSocket(path = "/endpoint/{username}") +class MyEndpoint { + + @Inject + CoolService service; + + @OnOpen + void open(WebSocketConnection connection) { + connection.userData().put(TypedKey.forBoolean("isCool"), service.isCool(connection.pathParam("username"))); <1> + } + + @OnTextMessage + String process(String message) { + if (connection.userData().get(TypedKey.forBoolean("isCool"))) { <2> + return "Cool message processed!"; + } else { + return "Message processed!"; + } + } +} +---- +<1> `CoolService#isCool()` returns `Boolean` that is associated with the current connection. +<2> The `TypedKey.forBoolean("isCool")` is the key used to obtain the data stored when the connection was created. + [[server-cdi-events]] ==== CDI events @@ -997,6 +1031,41 @@ class MyBean { There are also other convenient methods. For example, `OpenClientConnections#findByClientId(String)` makes it easy to find connections for a specific endpoint. +==== User data + +It is also possible to associate arbitrary user data with a specific connection. +The `io.quarkus.websockets.next.UserData` object obtained by the `WebSocketClientConnection#userData()` method represents mutable user data associated with a connection. + +[source, java] +---- +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.UserData.TypedKey; + +@WebSocketClient(path = "/endpoint/{username}") +class MyEndpoint { + + @Inject + CoolService service; + + @OnOpen + void open(WebSocketClientConnection connection) { + connection.userData().put(TypedKey.forBoolean("isCool"), service.isCool(connection.pathParam("username"))); <1> + } + + @OnTextMessage + String process(String message) { + if (connection.userData().get(TypedKey.forBoolean("isCool"))) { <2> + return "Cool message processed!"; + } else { + return "Message processed!"; + } + } +} +---- +<1> `CoolService#isCool()` returns `Boolean` that is associated with the current connection. +<2> The `TypedKey.forBoolean("isCool")` is the key used to obtain the data stored when the connection was created. + + [[client-cdi-events]] ==== CDI events diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/connection/ConnectionUserDataTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/connection/ConnectionUserDataTest.java new file mode 100644 index 0000000000000..1118c55e90e58 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/connection/ConnectionUserDataTest.java @@ -0,0 +1,92 @@ +package io.quarkus.websockets.next.test.connection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URI; +import java.util.List; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.OpenConnections; +import io.quarkus.websockets.next.UserData.TypedKey; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class ConnectionUserDataTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(MyEndpoint.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("/end") + URI baseUri; + + @Inject + OpenConnections connections; + + @Test + void testConnectionData() { + try (WSClient client = WSClient.create(vertx).connect(baseUri)) { + assertEquals("5", client.sendAndAwaitReply("bar").toString()); + assertNotNull(connections.stream().filter(c -> c.userData().get(TypedKey.forString("username")) != null).findFirst() + .orElse(null)); + assertEquals("FOOMartin", client.sendAndAwaitReply("foo").toString()); + assertEquals("0", client.sendAndAwaitReply("bar").toString()); + } + } + + @WebSocket(path = "/end") + public static class MyEndpoint { + + @OnOpen + void onOpen(WebSocketConnection connection) { + connection.userData().put(TypedKey.forInt("baz"), 5); + connection.userData().put(TypedKey.forLong("foo"), 42l); + connection.userData().put(TypedKey.forString("username"), "Martin"); + connection.userData().put(TypedKey.forBoolean("isActive"), true); + connection.userData().put(new TypedKey>("list"), List.of()); + } + + @OnTextMessage + public String onMessage(String message, WebSocketConnection connection) { + if ("bar".equals(message)) { + return connection.userData().size() + ""; + } + try { + connection.userData().get(TypedKey.forString("foo")).toString(); + throw new IllegalStateException(); + } catch (ClassCastException expected) { + } + if (!connection.userData().get(TypedKey.forBoolean("isActive")) + || !connection.userData().get(new TypedKey>("list")).isEmpty()) { + return "NOK"; + } + if (connection.userData().remove(TypedKey.forLong("foo")) != 42l) { + throw new IllegalStateException(); + } + if (connection.userData().remove(TypedKey.forInt("baz")) != 5) { + throw new IllegalStateException(); + } + String ret = message.toUpperCase() + connection.userData().get(TypedKey.forString("username")); + connection.userData().clear(); + return ret; + } + + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/Connection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/Connection.java new file mode 100644 index 0000000000000..6a249c815a23c --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/Connection.java @@ -0,0 +1,103 @@ +package io.quarkus.websockets.next; + +import java.time.Instant; + +import io.smallrye.common.annotation.CheckReturnValue; +import io.smallrye.mutiny.Uni; + +/** + * WebSocket connection. + * + * @see WebSocketConnection + * @see WebSocketClientConnection + */ +public interface Connection extends BlockingSender { + + /** + * + * @return the unique identifier assigned to this connection + */ + String id(); + + /** + * + * @param name + * @return the value of the path parameter or {@code null} + * @see WebSocketClient#path() + */ + String pathParam(String name); + + /** + * @return {@code true} if the HTTP connection is encrypted via SSL/TLS + */ + boolean isSecure(); + + /** + * @return {@code true} if the WebSocket is closed + */ + boolean isClosed(); + + /** + * + * @return the close reason or {@code null} if the connection is not closed + */ + CloseReason closeReason(); + + /** + * + * @return {@code true} if the WebSocket is open + */ + default boolean isOpen() { + return !isClosed(); + } + + /** + * Close the connection. + * + * @return a new {@link Uni} with a {@code null} item + */ + @CheckReturnValue + default Uni close() { + return close(CloseReason.NORMAL); + } + + /** + * Close the connection with a specific reason. + * + * @param reason + * @return a new {@link Uni} with a {@code null} item + */ + Uni close(CloseReason reason); + + /** + * Close the connection and wait for the completion. + */ + default void closeAndAwait() { + close().await().indefinitely(); + } + + /** + * Close the connection with a specific reason and wait for the completion. + */ + default void closeAndAwait(CloseReason reason) { + close(reason).await().indefinitely(); + } + + /** + * + * @return the handshake request + */ + HandshakeRequest handshakeRequest(); + + /** + * + * @return the time when this connection was created + */ + Instant creationTime(); + + /** + * + * @return the user data associated with this connection + */ + UserData userData(); +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UserData.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UserData.java new file mode 100644 index 0000000000000..9f09bba8ff7f6 --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/UserData.java @@ -0,0 +1,59 @@ +package io.quarkus.websockets.next; + +/** + * Mutable user data associated with a connection. Implementations must be thread-safe. + */ +public interface UserData { + + /** + * + * @param + * @param key + * @return the value or {@code null} if no mapping is found + */ + VALUE get(TypedKey key); + + /** + * Associates the specified value with the specified key. An old value is replaced by the specified value. + * + * @param + * @param key + * @param value + * @return the previous value associated with {@code key}, or {@code null} if no mapping exists + */ + VALUE put(TypedKey key, VALUE value); + + /** + * + * @param + * @param key + */ + VALUE remove(TypedKey key); + + int size(); + + void clear(); + + /** + * @param The type this key is used for. + */ + record TypedKey(String value) { + + public static TypedKey forInt(String key) { + return new TypedKey<>(key); + } + + public static TypedKey forLong(String key) { + return new TypedKey<>(key); + } + + public static TypedKey forString(String key) { + return new TypedKey<>(key); + } + + public static TypedKey forBoolean(String key) { + return new TypedKey<>(key); + } + } + +} \ No newline at end of file diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index 393ba422b7351..e33f95bea1e54 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java @@ -1,8 +1,6 @@ package io.quarkus.websockets.next; -import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; -import io.smallrye.mutiny.Uni; /** * This interface represents a client connection to a WebSocket endpoint. @@ -11,87 +9,11 @@ * endpoint and used to interact with the connected server. */ @Experimental("This API is experimental and may change in the future") -public interface WebSocketClientConnection extends Sender, BlockingSender { - - /** - * - * @return the unique identifier assigned to this connection - */ - String id(); +public interface WebSocketClientConnection extends Connection { /* * @return the client id */ String clientId(); - /** - * - * @param name - * @return the value of the path parameter or {@code null} - * @see WebSocketClient#path() - */ - String pathParam(String name); - - /** - * @return {@code true} if the HTTP connection is encrypted via SSL/TLS - */ - boolean isSecure(); - - /** - * @return {@code true} if the WebSocket is closed - */ - boolean isClosed(); - - /** - * - * @return the close reason or {@code null} if the connection is not closed - */ - CloseReason closeReason(); - - /** - * - * @return {@code true} if the WebSocket is open - */ - default boolean isOpen() { - return !isClosed(); - } - - /** - * Close the connection. - * - * @return a new {@link Uni} with a {@code null} item - */ - @CheckReturnValue - default Uni close() { - return close(CloseReason.NORMAL); - } - - /** - * Close the connection with a specific reason. - * - * @param reason - * @return a new {@link Uni} with a {@code null} item - */ - Uni close(CloseReason reason); - - /** - * Close the connection and wait for the completion. - */ - default void closeAndAwait() { - close().await().indefinitely(); - } - - /** - * Close the connection with a specific reason and wait for the completion. - */ - default void closeAndAwait(CloseReason reason) { - close(reason).await().indefinitely(); - } - - /** - * - * @return the handshake request - */ - HandshakeRequest handshakeRequest(); - } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index a63a3e2e5772e..c5deaa339b216 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -1,12 +1,9 @@ package io.quarkus.websockets.next; -import java.time.Instant; import java.util.Set; import java.util.function.Predicate; -import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; -import io.smallrye.mutiny.Uni; /** * This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server. @@ -19,13 +16,7 @@ * {@link BlockingSender} and {@link Sender} respectively. */ @Experimental("This API is experimental and may change in the future") -public interface WebSocketConnection extends Sender, BlockingSender { - - /** - * - * @return the unique identifier assigned to this connection - */ - String id(); +public interface WebSocketConnection extends Connection { /** * @@ -34,14 +25,6 @@ public interface WebSocketConnection extends Sender, BlockingSender { */ String endpointId(); - /** - * - * @param name - * @return the decoded value of the path parameter or {@code null} - * @see WebSocket#path() - */ - String pathParam(String name); - /** * Sends messages to all open clients connected to the same WebSocket endpoint. * @@ -57,86 +40,18 @@ public interface WebSocketConnection extends Sender, BlockingSender { */ Set getOpenConnections(); - /** - * @return {@code true} if the HTTP connection is encrypted via SSL/TLS - */ - boolean isSecure(); - - /** - * @return {@code true} if the WebSocket is closed - */ - boolean isClosed(); - - /** - * - * @return the close reason or {@code null} if the connection is not closed - */ - CloseReason closeReason(); - - /** - * - * @return {@code true} if the WebSocket is open - */ - default boolean isOpen() { - return !isClosed(); - } - - /** - * Close the connection. - * - * @return a new {@link Uni} with a {@code null} item - */ - @CheckReturnValue - default Uni close() { - return close(CloseReason.NORMAL); - } - - /** - * Close the connection with a specific reason. - * - * @param reason - * @return a new {@link Uni} with a {@code null} item - */ - Uni close(CloseReason reason); - - /** - * Close the connection and wait for the completion. - */ - default void closeAndAwait() { - close().await().indefinitely(); - } - - /** - * Close the connection and wait for the completion. - */ - default void closeAndAwait(CloseReason reason) { - close(reason).await().indefinitely(); - } - - /** - * - * @return the handshake request - */ - HandshakeRequest handshakeRequest(); - /** * * @return the subprotocol selected by the handshake */ String subprotocol(); - /** - * - * @return the time when this connection was created - */ - Instant creationTime(); - /** * Makes it possible to send messages to all clients connected to the same WebSocket endpoint. * * @see WebSocketConnection#getOpenConnections() */ - interface BroadcastSender extends Sender, BlockingSender { + interface BroadcastSender extends BlockingSender { /** * diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/UserDataImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/UserDataImpl.java new file mode 100644 index 0000000000000..a92f5192b2c3c --- /dev/null +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/UserDataImpl.java @@ -0,0 +1,44 @@ +package io.quarkus.websockets.next.runtime; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import io.quarkus.websockets.next.UserData; + +final class UserDataImpl implements UserData { + + private final ConcurrentMap data; + + UserDataImpl() { + this.data = new ConcurrentHashMap<>(); + } + + @SuppressWarnings("unchecked") + @Override + public VALUE get(TypedKey key) { + return (VALUE) data.get(key.value()); + } + + @SuppressWarnings("unchecked") + @Override + public VALUE put(TypedKey key, VALUE value) { + return (VALUE) data.put(key.value(), value); + } + + @SuppressWarnings("unchecked") + @Override + public VALUE remove(TypedKey key) { + return (VALUE) data.remove(key.value()); + } + + @Override + public void clear() { + data.clear(); + } + + @Override + public int size() { + return data.size(); + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java index 4febc7792d813..7293ef30319c7 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionBase.java @@ -8,7 +8,9 @@ import io.quarkus.vertx.utils.NoBoundChecksBuffer; import io.quarkus.websockets.next.CloseReason; +import io.quarkus.websockets.next.Connection; import io.quarkus.websockets.next.HandshakeRequest; +import io.quarkus.websockets.next.UserData; import io.quarkus.websockets.next.WebSocketConnection.BroadcastSender; import io.smallrye.mutiny.Uni; import io.vertx.core.buffer.Buffer; @@ -17,7 +19,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -public abstract class WebSocketConnectionBase { +public abstract class WebSocketConnectionBase implements Connection { private static final Logger LOG = Logger.getLogger(WebSocketConnectionBase.class); @@ -33,6 +35,8 @@ public abstract class WebSocketConnectionBase { protected final TrafficLogger trafficLogger; + private final UserData data; + WebSocketConnectionBase(Map pathParams, Codecs codecs, HandshakeRequest handshakeRequest, TrafficLogger trafficLogger) { this.identifier = UUID.randomUUID().toString(); @@ -41,18 +45,22 @@ public abstract class WebSocketConnectionBase { this.handshakeRequest = handshakeRequest; this.creationTime = Instant.now(); this.trafficLogger = trafficLogger; + this.data = new UserDataImpl(); } abstract WebSocketBase webSocket(); + @Override public String id() { return identifier; } + @Override public String pathParam(String name) { return pathParams.get(name); } + @Override public Uni sendText(String message) { Uni uni = Uni.createFrom().completionStage(() -> webSocket().writeTextMessage(message).toCompletionStage()); return trafficLogger == null ? uni : uni.invoke(() -> { @@ -60,11 +68,13 @@ public Uni sendText(String message) { }); } + @Override public Uni sendBinary(Buffer message) { Uni uni = Uni.createFrom().completionStage(() -> webSocket().writeBinaryMessage(message).toCompletionStage()); return trafficLogger == null ? uni : uni.invoke(() -> trafficLogger.binaryMessageSent(this, message)); } + @Override public Uni sendText(M message) { String text; // Use the same conversion rules as defined for the OnTextMessage @@ -79,6 +89,7 @@ public Uni sendText(M message) { return sendText(text); } + @Override public Uni sendPing(Buffer data) { return Uni.createFrom().completionStage(() -> webSocket().writePing(data).toCompletionStage()); } @@ -91,14 +102,17 @@ void sendAutoPing() { }); } + @Override public Uni sendPong(Buffer data) { return Uni.createFrom().completionStage(() -> webSocket().writePong(data).toCompletionStage()); } + @Override public Uni close() { return close(CloseReason.NORMAL); } + @Override public Uni close(CloseReason reason) { if (isClosed()) { LOG.warnf("Connection already closed: %s", this); @@ -108,18 +122,22 @@ public Uni close(CloseReason reason) { .completionStage(() -> webSocket().close((short) reason.getCode(), reason.getMessage()).toCompletionStage()); } + @Override public boolean isSecure() { return webSocket().isSsl(); } + @Override public boolean isClosed() { return webSocket().isClosed(); } + @Override public HandshakeRequest handshakeRequest() { return handshakeRequest; } + @Override public Instant creationTime() { return creationTime; } @@ -128,6 +146,7 @@ public BroadcastSender broadcast() { throw new UnsupportedOperationException(); } + @Override public CloseReason closeReason() { WebSocketBase ws = webSocket(); if (ws.isClosed()) { @@ -140,4 +159,10 @@ public CloseReason closeReason() { } return null; } + + @Override + public UserData userData() { + return data; + } + } From e2fa52c94d8d7997b5f2861b02fa7a526ae45f2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:54:47 +0000 Subject: [PATCH 6/7] Bump io.quarkus.develocity:quarkus-project-develocity-extension Bumps [io.quarkus.develocity:quarkus-project-develocity-extension](https://github.com/quarkusio/quarkus-project-develocity-extension) from 1.1.5 to 1.1.6. - [Commits](https://github.com/quarkusio/quarkus-project-develocity-extension/compare/1.1.5...1.1.6) --- updated-dependencies: - dependency-name: io.quarkus.develocity:quarkus-project-develocity-extension dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 40090b0766681..a5cde63603c09 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -17,6 +17,6 @@ io.quarkus.develocity quarkus-project-develocity-extension - 1.1.5 + 1.1.6 From 186e0ed198ec556657a683b3d810d3f32af9a32e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:37:59 +0000 Subject: [PATCH 7/7] Bump manusa/actions-setup-minikube from 2.12.0 to 2.13.0 Bumps [manusa/actions-setup-minikube](https://github.com/manusa/actions-setup-minikube) from 2.12.0 to 2.13.0. - [Release notes](https://github.com/manusa/actions-setup-minikube/releases) - [Commits](https://github.com/manusa/actions-setup-minikube/compare/v2.12.0...v2.13.0) --- updated-dependencies: - dependency-name: manusa/actions-setup-minikube dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-istio.yml | 2 +- .github/workflows/ci-kubernetes.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-istio.yml b/.github/workflows/ci-istio.yml index 0606237f39898..ed3970535fbcf 100644 --- a/.github/workflows/ci-istio.yml +++ b/.github/workflows/ci-istio.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: tar -xzf maven-repo.tgz -C ~ - name: Set up Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.12.0 + uses: manusa/actions-setup-minikube@v2.13.0 with: minikube version: v1.16.0 kubernetes version: ${{ matrix.kubernetes }} diff --git a/.github/workflows/ci-kubernetes.yml b/.github/workflows/ci-kubernetes.yml index 7f3bb465ba8ad..6229c70502d7e 100644 --- a/.github/workflows/ci-kubernetes.yml +++ b/.github/workflows/ci-kubernetes.yml @@ -57,7 +57,7 @@ jobs: shell: bash run: tar -xzf maven-repo.tgz -C ~ - name: Set up Minikube-Kubernetes - uses: manusa/actions-setup-minikube@v2.12.0 + uses: manusa/actions-setup-minikube@v2.13.0 with: minikube version: v1.16.0 kubernetes version: ${{ matrix.kubernetes }}