From b7959e3fe884f5f158ac74c9195bf77c3b311bc6 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 24 Jul 2025 10:58:34 +0300 Subject: [PATCH 1/4] Polish SpringDataJPAProcessor --- .../deployment/SpringDataJPAProcessor.java | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java index c3dbb516942f3..296fdd0c8e147 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java @@ -173,48 +173,39 @@ private void detectAndLogSpecificSpringPropertiesIfExist() { Iterable iterablePropertyNames = config.getPropertyNames(); List propertyNames = new ArrayList(); iterablePropertyNames.forEach(propertyNames::add); - List springProperties = propertyNames.stream().filter(s -> pattern.matcher(s).matches()).collect(toList()); + List springProperties = propertyNames.stream().filter(s -> pattern.matcher(s).matches()).toList(); String notSupportedProperties = ""; if (!springProperties.isEmpty()) { for (String sp : springProperties) { - switch (sp) { - case SPRING_JPA_SHOW_SQL: - notSupportedProperties = notSupportedProperties + "\t- " + SPRING_JPA_SHOW_SQL - + " should be replaced by " + QUARKUS_HIBERNATE_ORM_LOG_SQL + "\n"; - break; - case SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: - notSupportedProperties = notSupportedProperties + "\t- " + SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT + notSupportedProperties = switch (sp) { + case SPRING_JPA_SHOW_SQL -> notSupportedProperties + "\t- " + SPRING_JPA_SHOW_SQL + + " should be replaced by " + QUARKUS_HIBERNATE_ORM_LOG_SQL + "\n"; + case SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT -> + notSupportedProperties + "\t- " + SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT + " should be replaced by " + QUARKUS_HIBERNATE_ORM_DIALECT + "\n"; - break; - case SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT_STORAGE_ENGINE: - notSupportedProperties = notSupportedProperties + "\t- " - + SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT_STORAGE_ENGINE + " should be replaced by " - + QUARKUS_HIBERNATE_ORM_DIALECT_STORAGE_ENGINE + "\n"; - break; - case SPRING_JPA_GENERATE_DDL: - notSupportedProperties = notSupportedProperties + "\t- " + SPRING_JPA_GENERATE_DDL - + " should be replaced by " + QUARKUS_HIBERNATE_ORM_SCHEMA_MANAGEMENT_STRATEGY + "\n"; - break; - case SPRING_JPA_HIBERNATE_NAMING_PHYSICAL_STRATEGY: - notSupportedProperties = notSupportedProperties + "\t- " + SPRING_JPA_HIBERNATE_NAMING_PHYSICAL_STRATEGY + case SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT_STORAGE_ENGINE -> notSupportedProperties + "\t- " + + SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT_STORAGE_ENGINE + + " should be replaced by " + + QUARKUS_HIBERNATE_ORM_DIALECT_STORAGE_ENGINE + + "\n"; + case SPRING_JPA_GENERATE_DDL -> notSupportedProperties + "\t- " + SPRING_JPA_GENERATE_DDL + + " should be replaced by " + + QUARKUS_HIBERNATE_ORM_SCHEMA_MANAGEMENT_STRATEGY + "\n"; + case SPRING_JPA_HIBERNATE_NAMING_PHYSICAL_STRATEGY -> + notSupportedProperties + "\t- " + SPRING_JPA_HIBERNATE_NAMING_PHYSICAL_STRATEGY + " should be replaced by " + QUARKUS_HIBERNATE_ORM_PHYSICAL_NAMING_STRATEGY + "\n"; - break; - case SPRING_JPA_HIBERNATE_NAMING_IMPLICIT_STRATEGY: - notSupportedProperties = notSupportedProperties + "\t- " + SPRING_JPA_HIBERNATE_NAMING_IMPLICIT_STRATEGY + case SPRING_JPA_HIBERNATE_NAMING_IMPLICIT_STRATEGY -> + notSupportedProperties + "\t- " + SPRING_JPA_HIBERNATE_NAMING_IMPLICIT_STRATEGY + " should be replaced by " + QUARKUS_HIBERNATE_ORM_IMPLICIT_NAMING_STRATEGY + "\n"; - break; - case SPRING_DATASOURCE_DATA: - notSupportedProperties = notSupportedProperties + "\t- " + QUARKUS_HIBERNATE_ORM_SQL_LOAD_SCRIPT + case SPRING_DATASOURCE_DATA -> + notSupportedProperties + "\t- " + QUARKUS_HIBERNATE_ORM_SQL_LOAD_SCRIPT + " could be used to load data instead of " + SPRING_DATASOURCE_DATA + " but it does not support ant-style patterns as " + SPRING_DATASOURCE_DATA + " does, it accepts the name of files containing the SQL statements to execute when Hibernate ORM starts.\n"; - break; - default: - notSupportedProperties = notSupportedProperties + "\t- " + sp + " does not have a Quarkus equivalent\n"; - break; - } + default -> notSupportedProperties + "\t- " + sp + " does not have a Quarkus equivalent\n"; + }; } LOGGER.warnf( "Quarkus does not support the following Spring Boot configuration properties: %n%s", From 4e4ab1b8c40dd1cc6dab276a7b493c78104d1b43 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Mon, 28 Jul 2025 20:42:07 +1000 Subject: [PATCH 2/4] Dev Assistant: Allow data creation when table is empty Signed-off-by: Phillip Kruger --- .../devui/AgroalDevUIProcessor.java | 20 ++--- .../resources/dev-ui/qwc-agroal-datasource.js | 49 ++++++++++-- extensions/agroal/runtime-dev/pom.xml | 4 + .../runtime/dev/ui/DatabaseInspector.java | 75 +++++++++++++++++++ 4 files changed, 132 insertions(+), 16 deletions(-) diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/devui/AgroalDevUIProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/devui/AgroalDevUIProcessor.java index f30ac4e5e0d2a..794a5999a28c2 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/devui/AgroalDevUIProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/devui/AgroalDevUIProcessor.java @@ -43,16 +43,18 @@ void devUI(DataSourcesJdbcBuildTimeConfig config, void createBuildTimeActions(BuildProducer buildTimeActionProducer) { BuildTimeActionBuildItem bta = new BuildTimeActionBuildItem(); - // TODO: If currentInsertScript is empty, maybe send tables schema. This might mean we need to move this to runtime + bta.actionBuilder() + .methodName("generateMoreData") + .assistantFunction((a, p) -> { + Assistant assistant = (Assistant) a; + return assistant.assistBuilder() + .userMessage(ADD_DATA_MESSAGE) + .variables(p) + .assist(); + }).build(); - bta.addAssistantAction("generateMoreData", (a, p) -> { - Assistant assistant = (Assistant) a; - return assistant.assistBuilder() - .userMessage(USER_MESSAGE) - .variables(p) - .assist(); - }); buildTimeActionProducer.produce(bta); + } @BuildStep @@ -60,7 +62,7 @@ JsonRPCProvidersBuildItem createJsonRPCService() { return new JsonRPCProvidersBuildItem(DatabaseInspector.class); } - private static final String USER_MESSAGE = """ + private static final String ADD_DATA_MESSAGE = """ Given the provided sql script: {{currentInsertScript}} Can you add 10 more inserts into the script and return the result diff --git a/extensions/agroal/deployment/src/main/resources/dev-ui/qwc-agroal-datasource.js b/extensions/agroal/deployment/src/main/resources/dev-ui/qwc-agroal-datasource.js index cc07c85c21633..3de2bf0f51750 100644 --- a/extensions/agroal/deployment/src/main/resources/dev-ui/qwc-agroal-datasource.js +++ b/extensions/agroal/deployment/src/main/resources/dev-ui/qwc-agroal-datasource.js @@ -157,6 +157,10 @@ export class QwcAgroalDatasource extends observeState(QwcHotReloadElement) { text-decoration: none; color: var(--lumo-primary-text-color); } + .generateTableDataButton { + align-self: center; + padding-top: 50px; + } `; static properties = { @@ -365,6 +369,29 @@ export class QwcAgroalDatasource extends observeState(QwcHotReloadElement) { } } + _generateInitialData(){ + this._showBusyLoadingDialog = "Quarkus Assistant is generating data ... please wait"; + this.jsonRpc.generateTableData({ + datasource:this._selectedDataSource.name, + schema: this._selectedTable.tableSchema, + name: this._selectedTable.tableName, + rowCount: 5 + }).then(jsonRpcResponse => { + const script = jsonRpcResponse.result.script; + if (Array.isArray(script)) { + this._currentSQL = script.join('\n'); + } else { + this._currentSQL = script; + } + this._showBusyLoadingDialog = null; + this._showImportSQLDialog = true; + this._showAssistantWarning = true; + + + + }); + } + _saveInsertScript(){ try { const blob = new Blob([this.value], { type: 'text/sql' }); @@ -525,13 +552,7 @@ export class QwcAgroalDatasource extends observeState(QwcHotReloadElement) { if(this._selectedTable && this._currentDataSet && this._currentDataSet.cols){ return html`
${this._renderSqlInput()} - - ${this._currentDataSet.cols.map((col) => - this._renderTableHeader(col) - )} - No data. - - ${this._renderPager()} + ${this._renderTableDataGrid()}
`; }else if(this._displaymessage){ @@ -544,6 +565,20 @@ export class QwcAgroalDatasource extends observeState(QwcHotReloadElement) { } } + _renderTableDataGrid(){ + if((this._currentDataSet.data && this._currentDataSet.data.length>0)|| !assistantState.current.isConfigured){ + return html` + ${this._currentDataSet.cols.map((col) => + this._renderTableHeader(col) + )} + No data. + + ${this._renderPager()}`; + }else { + return html`Generate some data`; + } + } + _renderTableHeader(col){ let heading = col; if(this._selectedTable.primaryKeys.includes(col)){ diff --git a/extensions/agroal/runtime-dev/pom.xml b/extensions/agroal/runtime-dev/pom.xml index a5f8c9be98dc6..c9f52505774ef 100644 --- a/extensions/agroal/runtime-dev/pom.xml +++ b/extensions/agroal/runtime-dev/pom.xml @@ -18,5 +18,9 @@ ${project.groupId} quarkus-agroal + + io.quarkus + quarkus-assistant-dev + \ No newline at end of file diff --git a/extensions/agroal/runtime-dev/src/main/java/io/quarkus/agroal/runtime/dev/ui/DatabaseInspector.java b/extensions/agroal/runtime-dev/src/main/java/io/quarkus/agroal/runtime/dev/ui/DatabaseInspector.java index 83c678b6095b2..2da41df09bfc6 100644 --- a/extensions/agroal/runtime-dev/src/main/java/io/quarkus/agroal/runtime/dev/ui/DatabaseInspector.java +++ b/extensions/agroal/runtime-dev/src/main/java/io/quarkus/agroal/runtime/dev/ui/DatabaseInspector.java @@ -19,6 +19,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import jakarta.annotation.PostConstruct; import jakarta.enterprise.inject.Instance; @@ -34,6 +37,7 @@ import io.quarkus.agroal.runtime.AgroalDataSourceUtil; import io.quarkus.arc.InactiveBeanException; import io.quarkus.arc.InjectableInstance; +import io.quarkus.assistant.runtime.dev.Assistant; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.runtime.LaunchMode; @@ -44,6 +48,9 @@ public final class DatabaseInspector { @Inject Instance agroalDataSourceSupports; + @Inject + Optional assistant; + private final Map checkedDataSources = new HashMap<>(); private boolean isDev = false; @@ -333,6 +340,22 @@ public String getInsertScript(String datasource) { return null; } + public CompletionStage> generateTableData(String datasource, String schema, String name, int rowCount) { + if (isDev && assistant.isPresent()) { + List tables = getTables(datasource); + Optional
matchingTable = tables.stream() + .filter(t -> t.tableSchema().equals(schema) && t.tableName().equals(name)) + .findFirst(); + + if (matchingTable.isPresent()) { + return assistant.get().assistBuilder() + .userMessage(generateInsertPrompt(matchingTable.get(), rowCount)) + .assist(); + } + } + return CompletableFuture.failedStage(new RuntimeException("Assistant is not available")); + } + private void exportTable(Connection conn, StringWriter writer, String tableName) throws SQLException, IOException { try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName)) { @@ -454,6 +477,58 @@ private boolean isBinary(int dataType) { dataType == Types.OTHER; } + private String generateInsertPrompt(Table table, int rowCount) { + StringBuilder sb = new StringBuilder(); + sb.append("Generate a valid SQL script with ") + .append(rowCount) + .append(" INSERT statements for the following table:\n\n"); + + sb.append("Table name: ") + .append(table.tableSchema()) + .append(".") + .append(table.tableName()) + .append("\n\n"); + + sb.append("Columns:\n"); + for (Column column : table.columns()) { + sb.append("- ") + .append(column.columnName()) + .append(" (") + .append(column.columnType()); + if (column.columnType().equalsIgnoreCase("varchar")) { + sb.append("(").append(column.columnSize()).append(")"); + } + sb.append(", nullable: ").append(column.nullable()); + sb.append(")\n"); + } + + if (!table.primaryKeys().isEmpty()) { + sb.append("\nPrimary key(s): ").append(String.join(", ", table.primaryKeys())).append("\n"); + } + + if (!table.foreignKeys().isEmpty()) { + sb.append("\nForeign keys:\n"); + for (ForeignKey fk : table.foreignKeys()) { + sb.append("- ") + .append(fk.columnName()) + .append(" references ") + .append(fk.referencedTable()) + .append("(") + .append(fk.referencedColumn()) + .append(")\n"); + } + } + + sb.append( + "\nReturn the output in a field called `script` with the contents being a SQL script with valid INSERT INTO statements for ") + .append(table.tableSchema()) + .append(".") + .append(table.tableName()) + .append(".\n"); + + return sb.toString(); + } + private static record Column(String columnName, String columnType, int columnSize, String nullable, boolean binary) { } From f4256149a9578bfbba3002878ae0edc240a4af61 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 29 Jul 2025 10:06:09 +1000 Subject: [PATCH 3/4] Update assistant documentation with the dependency details needed Signed-off-by: Phillip Kruger --- docs/src/main/asciidoc/assistant.adoc | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/assistant.adoc b/docs/src/main/asciidoc/assistant.adoc index 8eb20600cc202..3e493d0cb1189 100644 --- a/docs/src/main/asciidoc/assistant.adoc +++ b/docs/src/main/asciidoc/assistant.adoc @@ -174,7 +174,17 @@ You can invoke the assistant from backend code using xref:dev-ui.adoc#communicat ==== JsonRPC against the Runtime classpath -To use the assistant in a xref:dev-ui.adoc#jsonrpc-against-the-runtime-classpath[`JsonRpcService`] on the runtime classpath, inject the `Assistant` like this: +To use the assistant in a xref:dev-ui.adoc#jsonrpc-against-the-runtime-classpath[`JsonRpcService`] on the runtime classpath, you need to make sure the `Assistant` interface is available: + +[source,xml] +---- + + io.quarkus + quarkus-assistant-dev + +---- + +Then you can inject the `Assistant` like this: [source,java] ---- @@ -201,7 +211,17 @@ You can now use this assistant in any JsonRPC method, example: ==== JsonRPC against the Deployment classpath -In xref:dev-ui.adoc#jsonrpc-against-the-deployment-classpath[deployment-time] code, use the `BuildTimeActionBuildItem` and register assistant actions via `.addAssistantAction(...)`: +In xref:dev-ui.adoc#jsonrpc-against-the-deployment-classpath[deployment-time] code, you need to add the assistant-deployment-spi: + +[source,xml] +---- + + io.quarkus + quarkus-assistant-deployment-spi + +---- + +Then use the `BuildTimeActionBuildItem` and register assistant actions via `.addAssistantAction(...)`: [source,java] ---- From 369a9d59cd8b5d8fcbd8c03ab0e3bcfb0f7186c3 Mon Sep 17 00:00:00 2001 From: Erik Mattheis Date: Wed, 18 Jun 2025 12:50:03 -0400 Subject: [PATCH 4/4] expose path params and user data to upgrade check --- .../HttpUpgradeCheckPathParamsTest.java | 95 +++++++++++++++++++ .../upgrade/HttpUpgradeCheckUserDataTest.java | 80 ++++++++++++++++ .../websockets/next/HttpUpgradeCheck.java | 16 ++++ .../next/runtime/HttpUpgradeContextImpl.java | 8 +- .../next/runtime/WebSocketConnectionImpl.java | 5 +- .../next/runtime/WebSocketServerRecorder.java | 16 ++-- 6 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckPathParamsTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckUserDataTest.java diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckPathParamsTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckPathParamsTest.java new file mode 100644 index 0000000000000..a414979bd879f --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckPathParamsTest.java @@ -0,0 +1,95 @@ +package io.quarkus.websockets.next.test.upgrade; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.HttpUpgradeCheck; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.http.UpgradeRejectedException; + +public class HttpUpgradeCheckPathParamsTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Endpoint.class, UpgradeCheck.class, WSClient.class)); + + @Inject + Vertx vertx; + + @TestHTTPResource("accept") + URI acceptUri; + + @TestHTTPResource("reject") + URI rejectUri; + + @BeforeEach + public void cleanUp() { + Endpoint.OPENED.set(false); + } + + @Test + public void testHttpUpgradeRejected() { + try (WSClient client = new WSClient(vertx)) { + CompletionException ce = assertThrows(CompletionException.class, + () -> client.connect(rejectUri)); + Throwable root = ExceptionUtil.getRootCause(ce); + assertInstanceOf(UpgradeRejectedException.class, root); + assertTrue(root.getMessage().contains("404"), root.getMessage()); + } + } + + @Test + public void testHttpUpgradePermitted() { + try (WSClient client = new WSClient(vertx)) { + client.connect(acceptUri); + Awaitility.await().atMost(Duration.ofSeconds(2)).until(Endpoint.OPENED::get); + } + } + + @WebSocket(path = "/{action}") + public static class Endpoint { + + static final AtomicBoolean OPENED = new AtomicBoolean(); + + @OnOpen + void onOpen() { + OPENED.set(true); + } + + } + + @Singleton + public static class UpgradeCheck implements HttpUpgradeCheck { + + @Override + public Uni perform(HttpUpgradeContext context) { + if ("reject".equals(context.pathParam("action"))) { + return CheckResult.rejectUpgrade(404); + } + return CheckResult.permitUpgrade(); + } + + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckUserDataTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckUserDataTest.java new file mode 100644 index 0000000000000..eb5118683ed1c --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/upgrade/HttpUpgradeCheckUserDataTest.java @@ -0,0 +1,80 @@ +package io.quarkus.websockets.next.test.upgrade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +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.HttpUpgradeCheck; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.UserData; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; + +public class HttpUpgradeCheckUserDataTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Endpoint.class, UpgradeCheck.class, WSClient.class)); + + @Inject + Vertx vertx; + + @TestHTTPResource("endpoint") + URI endpointUri; + + @Test + public void testHttpUpgradeCheckPassesUserData() throws InterruptedException { + try (WSClient client = new WSClient(vertx)) { + client.connect(endpointUri); + assertTrue(Endpoint.OPEN.await(2, TimeUnit.SECONDS)); + assertTrue(Endpoint.USER_DATA.get().get(UserData.TypedKey.forBoolean("boolean"))); + assertEquals(Integer.MAX_VALUE, Endpoint.USER_DATA.get().get(UserData.TypedKey.forInt("int"))); + assertEquals(Long.MAX_VALUE, Endpoint.USER_DATA.get().get(UserData.TypedKey.forLong("long"))); + assertEquals("Hello", Endpoint.USER_DATA.get().get(UserData.TypedKey.forString("string"))); + } + } + + @WebSocket(path = "/endpoint") + public static class Endpoint { + + static final CountDownLatch OPEN = new CountDownLatch(1); + static final AtomicReference USER_DATA = new AtomicReference<>(); + + @OnOpen + void onOpen(WebSocketConnection connection) { + USER_DATA.set(connection.userData()); + OPEN.countDown(); + } + + } + + @Singleton + public static class UpgradeCheck implements HttpUpgradeCheck { + + @Override + public Uni perform(HttpUpgradeContext context) { + context.userData().put(UserData.TypedKey.forBoolean("boolean"), true); + context.userData().put(UserData.TypedKey.forInt("int"), Integer.MAX_VALUE); + context.userData().put(UserData.TypedKey.forLong("long"), Long.MAX_VALUE); + context.userData().put(UserData.TypedKey.forString("string"), "Hello"); + return CheckResult.permitUpgrade(); + } + + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HttpUpgradeCheck.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HttpUpgradeCheck.java index efd57cf2d7c9e..6da6519ef01db 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HttpUpgradeCheck.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/HttpUpgradeCheck.java @@ -9,6 +9,7 @@ import io.quarkus.vertx.VertxContextSupport; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.RoutingContext; /** * A check that controls which requests are allowed to upgrade the HTTP connection to a WebSocket connection. @@ -45,6 +46,21 @@ default boolean appliesTo(String endpointId) { interface HttpUpgradeContext { + /** + * @return userData {@link UserData}; mutable user data to associate with an upgraded connection + * @see WebSocketConnection#userData() + */ + UserData userData(); + + /** + * Gets the value of a single path parameter + * + * @param name the name of parameter as defined in path declaration + * @return the actual value of the parameter or null if it doesn't exist + * @see RoutingContext#pathParam(String) + */ + String pathParam(String name); + /** * @return httpRequest {@link HttpServerRequest}; the HTTP 1.X request employing the 'Upgrade' header */ diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/HttpUpgradeContextImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/HttpUpgradeContextImpl.java index dc0cb5ce684c6..fa9225502e8c0 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/HttpUpgradeContextImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/HttpUpgradeContextImpl.java @@ -2,13 +2,19 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.websockets.next.HttpUpgradeCheck; +import io.quarkus.websockets.next.UserData; import io.smallrye.mutiny.Uni; import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; -record HttpUpgradeContextImpl(RoutingContext routingContext, +record HttpUpgradeContextImpl(RoutingContext routingContext, UserData userData, Uni securityIdentity, String endpointId) implements HttpUpgradeCheck.HttpUpgradeContext { + @Override + public String pathParam(String name) { + return routingContext.pathParam(name); + } + @Override public HttpServerRequest httpRequest() { return routingContext.request(); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java index c14047c43f2e9..5958859af3d5d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import io.quarkus.websockets.next.HandshakeRequest; +import io.quarkus.websockets.next.UserData; import io.quarkus.websockets.next.WebSocketConnection; import io.quarkus.websockets.next.runtime.telemetry.SendingInterceptor; import io.smallrye.mutiny.Uni; @@ -37,10 +38,10 @@ class WebSocketConnectionImpl extends WebSocketConnectionBase implements WebSock WebSocketConnectionImpl(String generatedEndpointClass, String endpointClass, ServerWebSocket webSocket, ConnectionManager connectionManager, Codecs codecs, RoutingContext ctx, - TrafficLogger trafficLogger, SendingInterceptor sendingInterceptor, + TrafficLogger trafficLogger, UserData userData, SendingInterceptor sendingInterceptor, Function securitySupportCreator) { super(Map.copyOf(ctx.pathParams()), codecs, new HandshakeRequestImpl(webSocket, ctx), trafficLogger, - new UserDataImpl(), sendingInterceptor); + userData, sendingInterceptor); this.generatedEndpointClass = generatedEndpointClass; this.endpointId = endpointClass; this.webSocket = Objects.requireNonNull(webSocket); diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index cd4fcdf94cc9d..7029a28e06822 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -38,6 +38,7 @@ import io.quarkus.websockets.next.HttpUpgradeCheck; import io.quarkus.websockets.next.HttpUpgradeCheck.CheckResult; import io.quarkus.websockets.next.HttpUpgradeCheck.HttpUpgradeContext; +import io.quarkus.websockets.next.UserData; import io.quarkus.websockets.next.WebSocketSecurity; import io.quarkus.websockets.next.WebSocketServerException; import io.quarkus.websockets.next.runtime.config.WebSocketsServerRuntimeConfig; @@ -93,20 +94,21 @@ public Handler createEndpointHandler(String generatedEndpointCla @Override public void handle(RoutingContext ctx) { if (ctx.request().headers().contains(HandshakeRequest.SEC_WEBSOCKET_KEY)) { + UserData userData = new UserDataImpl(); if (httpUpgradeChecks != null) { - checkHttpUpgrade(ctx, endpointId).subscribe().with(result -> { + checkHttpUpgrade(ctx, endpointId, userData).subscribe().with(result -> { if (!result.getResponseHeaders().isEmpty()) { result.getResponseHeaders().forEach((k, v) -> ctx.response().putHeader(k, v)); } if (result.isUpgradePermitted()) { - httpUpgrade(ctx); + httpUpgrade(ctx, userData); } else { ctx.response().setStatusCode(result.getHttpResponseCode()).end(); } }, ctx::fail); } else { - httpUpgrade(ctx); + httpUpgrade(ctx, userData); } } else { LOG.debugf("Non-websocket client request ignored:\n%s", ctx.request().headers()); @@ -114,7 +116,7 @@ public void handle(RoutingContext ctx) { } } - private void httpUpgrade(RoutingContext ctx) { + private void httpUpgrade(RoutingContext ctx, UserData userData) { var telemetrySupport = telemetryProvider == null ? null : telemetryProvider.createServerTelemetrySupport(endpointPath); final Future future; @@ -136,7 +138,7 @@ public void handle(Throwable throwable) { SendingInterceptor sendingInterceptor = telemetrySupport == null ? null : telemetrySupport.getSendingInterceptor(); WebSocketConnectionImpl connection = new WebSocketConnectionImpl(generatedEndpointClass, endpointId, ws, - connectionManager, codecs, ctx, trafficLogger, sendingInterceptor, + connectionManager, codecs, ctx, trafficLogger, userData, sendingInterceptor, getSecuritySupportCreator(container, ctx)); connectionManager.add(generatedEndpointClass, connection); if (trafficLogger != null) { @@ -151,7 +153,7 @@ public void handle(Throwable throwable) { }); } - private Uni checkHttpUpgrade(RoutingContext ctx, String endpointId) { + private Uni checkHttpUpgrade(RoutingContext ctx, String endpointId, UserData userData) { QuarkusHttpUser user = (QuarkusHttpUser) ctx.user(); Uni identity; if (user == null) { @@ -159,7 +161,7 @@ private Uni checkHttpUpgrade(RoutingContext ctx, String endpointId) } else { identity = Uni.createFrom().item(user.getSecurityIdentity()); } - return checkHttpUpgrade(new HttpUpgradeContextImpl(ctx, identity, endpointId), httpUpgradeChecks, 0); + return checkHttpUpgrade(new HttpUpgradeContextImpl(ctx, userData, identity, endpointId), httpUpgradeChecks, 0); } private static Uni checkHttpUpgrade(HttpUpgradeContext ctx,