From 74310d88d266998e901ffbe8c182351cde57e980 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Wed, 7 Dec 2022 09:09:30 +0100 Subject: [PATCH] fix: make sure annotated endpoints works along Vaadin PUSH (#86) The workaround added to make PUSH work in Quarkus prevents the usage of custom annotated websocket endpoint. This change allows them to work along Vaadin PUSH. Fixes #78 --- .../deployment/VaadinQuarkusProcessor.java | 4 +- integration-tests/common-test-code/pom.xml | 6 ++ .../quarkus/it/CustomAnnotatedEnpoint.java | 23 ++++++ .../flow/quarkus/it/CustomWebsocketsIT.java | 81 +++++++++++++++++++ .../custom-websocket-dependency/pom.xml | 57 +++++++++++++ .../websockets/DependencyAnnotatedWS.java | 23 ++++++ .../sample/websockets/SimpleEndpoint.java | 39 +++++++++ .../websockets/SimpleEndpointConfig.java | 21 +++++ integration-tests/development/pom.xml | 5 ++ .../development/vite.generated.ts | 24 +++--- integration-tests/pom.xml | 1 + integration-tests/production/pom.xml | 5 ++ .../production/vite.generated.ts | 24 +++--- .../com/vaadin/quarkus/EnableWebsockets.java | 21 ++++- 14 files changed, 311 insertions(+), 23 deletions(-) create mode 100644 integration-tests/common-test-code/src/main/java/com/vaadin/flow/quarkus/it/CustomAnnotatedEnpoint.java create mode 100644 integration-tests/common-test-code/src/test/java/com/vaadin/flow/quarkus/it/CustomWebsocketsIT.java create mode 100644 integration-tests/custom-websocket-dependency/pom.xml create mode 100644 integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/DependencyAnnotatedWS.java create mode 100644 integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpoint.java create mode 100644 integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpointConfig.java diff --git a/deployment/src/main/java/com/vaadin/quarkus/deployment/VaadinQuarkusProcessor.java b/deployment/src/main/java/com/vaadin/quarkus/deployment/VaadinQuarkusProcessor.java index 3c34d45..3bbd1f2 100644 --- a/deployment/src/main/java/com/vaadin/quarkus/deployment/VaadinQuarkusProcessor.java +++ b/deployment/src/main/java/com/vaadin/quarkus/deployment/VaadinQuarkusProcessor.java @@ -40,6 +40,7 @@ import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.websockets.client.deployment.ServerWebSocketContainerBuildItem; import io.quarkus.websockets.client.deployment.WebSocketDeploymentInfoBuildItem; +import org.atmosphere.cpr.ApplicationConfig; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; @@ -144,7 +145,8 @@ void mapVaadinServletPaths(final BeanArchiveIndexBuildItem beanArchiveIndex, .builder(QuarkusVaadinServlet.class.getName(), QuarkusVaadinServlet.class.getName()) .addMapping("/*").setAsyncSupported(true) - .setLoadOnStartup(1).build()); + .setLoadOnStartup(1) + .build()); } } diff --git a/integration-tests/common-test-code/pom.xml b/integration-tests/common-test-code/pom.xml index cb1219e..560a161 100644 --- a/integration-tests/common-test-code/pom.xml +++ b/integration-tests/common-test-code/pom.xml @@ -61,6 +61,12 @@ flow-test-util test + + com.vaadin + custom-websockets + ${project.version} + test + io.quarkus diff --git a/integration-tests/common-test-code/src/main/java/com/vaadin/flow/quarkus/it/CustomAnnotatedEnpoint.java b/integration-tests/common-test-code/src/main/java/com/vaadin/flow/quarkus/it/CustomAnnotatedEnpoint.java new file mode 100644 index 0000000..e47aaf8 --- /dev/null +++ b/integration-tests/common-test-code/src/main/java/com/vaadin/flow/quarkus/it/CustomAnnotatedEnpoint.java @@ -0,0 +1,23 @@ +package com.vaadin.flow.quarkus.it; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +@ServerEndpoint(CustomAnnotatedEnpoint.URI) +public class CustomAnnotatedEnpoint { + + public static final String URI = "/app-annotated-websocket"; + public static final String PREFIX = ">> Application Annotated Endpoint: "; + + @OnOpen + public void onOpen(Session session) { + session.getAsyncRemote().sendText(PREFIX + "Welcome"); + } + + @OnMessage + public void onMessage(String message, Session session) { + session.getAsyncRemote().sendText(PREFIX + message); + } +} diff --git a/integration-tests/common-test-code/src/test/java/com/vaadin/flow/quarkus/it/CustomWebsocketsIT.java b/integration-tests/common-test-code/src/test/java/com/vaadin/flow/quarkus/it/CustomWebsocketsIT.java new file mode 100644 index 0000000..91b1eb4 --- /dev/null +++ b/integration-tests/common-test-code/src/test/java/com/vaadin/flow/quarkus/it/CustomWebsocketsIT.java @@ -0,0 +1,81 @@ +package com.vaadin.flow.quarkus.it; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.vaadin.sample.websockets.DependencyAnnotatedWS; +import org.vaadin.sample.websockets.SimpleEndpoint; + +@QuarkusIntegrationTest +class CustomWebsocketsIT { + + @TestHTTPResource(DependencyAnnotatedWS.URI) + URI dependencyAnnotatedWSURI; + + @TestHTTPResource(SimpleEndpoint.URI) + URI dependencyNotAnnotatedWSURI; + + @TestHTTPResource(CustomAnnotatedEnpoint.URI) + URI appAnnotatedWSURI; + + @Test + void dependencyAnnotatedEndpointShouldWork() throws Exception { + assertWebsocketWorks(dependencyAnnotatedWSURI, + DependencyAnnotatedWS.PREFIX); + } + + @Test + void applicationAnnotatedEndpointShouldWork() throws Exception { + assertWebsocketWorks(appAnnotatedWSURI, CustomAnnotatedEnpoint.PREFIX); + } + + @Test + void dependencyNotAnnotatedEndpointShouldWork() throws Exception { + assertWebsocketWorks(dependencyNotAnnotatedWSURI, + SimpleEndpoint.PREFIX); + } + + void assertWebsocketWorks(URI uri, String messagePrefix) throws Exception { + Client client = new Client(); + try (Session session = ContainerProvider.getWebSocketContainer() + .connectToServer(client, uri)) { + Assertions.assertEquals("CONNECT", client.receivedMessage()); + Assertions.assertEquals(messagePrefix + "Welcome", + client.receivedMessage()); + session.getBasicRemote().sendText("hello world"); + Assertions.assertEquals(messagePrefix + "hello world", + client.receivedMessage()); + } + } + + @ClientEndpoint + public static class Client { + final LinkedBlockingDeque messages = new LinkedBlockingDeque<>(); + + @OnOpen + public void open(Session session) throws IOException { + messages.add("CONNECT"); + } + + @OnMessage + void message(String msg) { + messages.add(msg); + } + + String receivedMessage() throws InterruptedException { + return messages.poll(12, TimeUnit.SECONDS); + } + + } +} diff --git a/integration-tests/custom-websocket-dependency/pom.xml b/integration-tests/custom-websocket-dependency/pom.xml new file mode 100644 index 0000000..7d62ab2 --- /dev/null +++ b/integration-tests/custom-websocket-dependency/pom.xml @@ -0,0 +1,57 @@ + + + + + com.vaadin + vaadin-quarkus-integration-tests + 2.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + + custom-websockets + Test dependency with custom websocket endpoints + jar + + + true + true + + + + + jakarta.websocket + jakarta.websocket-api + 2.1.0 + provided + + + jakarta.websocket + jakarta.websocket-client-api + 2.1.0 + provided + + + + + + + org.jboss.jandex + jandex-maven-plugin + 1.2.2 + + + make-index + + jandex + + + + + + + diff --git a/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/DependencyAnnotatedWS.java b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/DependencyAnnotatedWS.java new file mode 100644 index 0000000..d75495f --- /dev/null +++ b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/DependencyAnnotatedWS.java @@ -0,0 +1,23 @@ +package org.vaadin.sample.websockets; + +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +@ServerEndpoint(DependencyAnnotatedWS.URI) +public class DependencyAnnotatedWS { + + public static final String URI = "/dependency-annotated-websocket"; + public static final String PREFIX = ">> Dependency Annotated Endpoint: "; + + @OnOpen + public void onOpen(Session session) { + session.getAsyncRemote().sendText(PREFIX + "Welcome"); + } + + @OnMessage + public void onMessage(String message, Session session) { + session.getAsyncRemote().sendText(PREFIX + message); + } +} diff --git a/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpoint.java b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpoint.java new file mode 100644 index 0000000..8657d5c --- /dev/null +++ b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpoint.java @@ -0,0 +1,39 @@ +package org.vaadin.sample.websockets; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.MessageHandler; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.Session; + +public class SimpleEndpoint extends Endpoint { + + public static final String URI = "/dependency-websocket"; + + public static final String PREFIX = ">> Dependency Simple Endpoint: "; + + @Override + public void onOpen(Session session, EndpointConfig config) { + Handler handler = new Handler(session.getAsyncRemote()); + session.addMessageHandler(handler); + handler.reply("Welcome"); + } + + private static class Handler implements MessageHandler.Whole { + + private final RemoteEndpoint.Async remote; + + public Handler(RemoteEndpoint.Async remote) { + this.remote = remote; + } + + @Override + public void onMessage(String message) { + reply(message); + } + + private void reply(String message) { + remote.sendText(PREFIX + message); + } + } +} diff --git a/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpointConfig.java b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpointConfig.java new file mode 100644 index 0000000..2103cb7 --- /dev/null +++ b/integration-tests/custom-websocket-dependency/src/main/java/org/vaadin/sample/websockets/SimpleEndpointConfig.java @@ -0,0 +1,21 @@ +package org.vaadin.sample.websockets; + +import jakarta.websocket.Endpoint; +import jakarta.websocket.server.ServerApplicationConfig; +import jakarta.websocket.server.ServerEndpointConfig; +import java.util.Collections; +import java.util.Set; + +public class SimpleEndpointConfig implements ServerApplicationConfig { + @Override + public Set getEndpointConfigs( + Set> endpointClasses) { + return Set.of(ServerEndpointConfig.Builder + .create(SimpleEndpoint.class, SimpleEndpoint.URI).build()); + } + + @Override + public Set> getAnnotatedEndpointClasses(Set> scanned) { + return Collections.emptySet(); + } +} diff --git a/integration-tests/development/pom.xml b/integration-tests/development/pom.xml index 5a5e082..89290a1 100644 --- a/integration-tests/development/pom.xml +++ b/integration-tests/development/pom.xml @@ -50,6 +50,11 @@ test-addon-with-jandex ${project.version} + + com.vaadin + custom-websockets + ${project.version} + com.vaadin diff --git a/integration-tests/development/vite.generated.ts b/integration-tests/development/vite.generated.ts index e30d55d..0c90ddf 100644 --- a/integration-tests/development/vite.generated.ts +++ b/integration-tests/development/vite.generated.ts @@ -5,11 +5,11 @@ * This file will be overwritten on every run. Any custom changes should be made to vite.config.ts */ import path from 'path'; -import { readFileSync, existsSync, writeFileSync } from 'fs'; +import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; import * as net from 'net'; -import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle'; -import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils'; +import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle.js'; +import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils.js'; import settings from './target/vaadin-dev-server-settings.json'; import { defineConfig, mergeConfig, PluginOption, ResolvedConfig, UserConfigFn, OutputOptions, AssetInfo, ChunkInfo } from 'vite'; import { getManifest } from 'workbox-build'; @@ -24,10 +24,13 @@ const appShellUrl = '.'; const frontendFolder = path.resolve(__dirname, settings.frontendFolder); const themeFolder = path.resolve(frontendFolder, settings.themeFolder); +const statsFolder = path.resolve(__dirname, settings.statsOutput); const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); -const addonFrontendFolder = path.resolve(__dirname, settings.addonFrontendFolder); +const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder); +const generatedFlowImportsFolder = path.resolve(__dirname, settings.generatedFlowImportsFolder); const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder); -const statsFile = path.resolve(frontendBundleFolder, '..', 'config', 'stats.json'); + +const statsFile = path.resolve(statsFolder, 'stats.json'); const projectStaticAssetsFolders = [ path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), @@ -40,7 +43,7 @@ const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.reso const themeOptions = { devMode: false, - // The following matches folder 'target/flow-frontend/themes/' + // The following matches folder 'frontend/generated/themes/' // (not 'frontend/themes') for theme in JAR that is copied there themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder), themeProjectFolders: themeProjectFolders, @@ -182,6 +185,7 @@ function statsExtracterPlugin(): PluginOption { .sort() .filter((value, index, self) => self.indexOf(value) === index); + mkdirSync(path.dirname(statsFile), { recursive: true }); writeFileSync(statsFile, JSON.stringify({ npmModules }, null, 1)); } }; @@ -460,8 +464,7 @@ let spaMiddlewareForceRemoved = false; const allowedFrontendFolders = [ frontendFolder, - addonFrontendFolder, - path.resolve(addonFrontendFolder, '..', 'frontend'), // Contains only generated-flow-imports + path.resolve(generatedFlowImportsFolder), // Contains only generated-flow-imports path.resolve(__dirname, 'node_modules') ]; @@ -500,6 +503,7 @@ export const vaadinConfig: UserConfigFn = (env) => { base: '', resolve: { alias: { + '@vaadin/flow-frontend': jarResourcesFolder, Frontend: frontendFolder }, preserveSymlinks: true @@ -563,14 +567,14 @@ export const vaadinConfig: UserConfigFn = (env) => { ] }), { - name: 'vaadin:force-remove-spa-middleware', + name: 'vaadin:force-remove-html-middleware', transformIndexHtml: { enforce: 'pre', transform(_html, { server }) { if (server && !spaMiddlewareForceRemoved) { server.middlewares.stack = server.middlewares.stack.filter((mw) => { const handleName = '' + mw.handle; - return !handleName.includes('viteSpaFallbackMiddleware'); + return !handleName.includes('viteHtmlFallbackMiddleware'); }); spaMiddlewareForceRemoved = true; } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 898ab43..4490239 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -19,6 +19,7 @@ test-addons/addon-with-jandex test-addons/addon-without-jandex + custom-websocket-dependency common-test-code diff --git a/integration-tests/production/pom.xml b/integration-tests/production/pom.xml index 3c43286..a48d2ea 100644 --- a/integration-tests/production/pom.xml +++ b/integration-tests/production/pom.xml @@ -45,6 +45,11 @@ test-addon-with-jandex ${project.version} + + com.vaadin + custom-websockets + ${project.version} + com.vaadin diff --git a/integration-tests/production/vite.generated.ts b/integration-tests/production/vite.generated.ts index e30d55d..0c90ddf 100644 --- a/integration-tests/production/vite.generated.ts +++ b/integration-tests/production/vite.generated.ts @@ -5,11 +5,11 @@ * This file will be overwritten on every run. Any custom changes should be made to vite.config.ts */ import path from 'path'; -import { readFileSync, existsSync, writeFileSync } from 'fs'; +import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; import * as net from 'net'; -import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle'; -import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils'; +import { processThemeResources } from './target/plugins/application-theme-plugin/theme-handle.js'; +import { rewriteCssUrls } from './target/plugins/theme-loader/theme-loader-utils.js'; import settings from './target/vaadin-dev-server-settings.json'; import { defineConfig, mergeConfig, PluginOption, ResolvedConfig, UserConfigFn, OutputOptions, AssetInfo, ChunkInfo } from 'vite'; import { getManifest } from 'workbox-build'; @@ -24,10 +24,13 @@ const appShellUrl = '.'; const frontendFolder = path.resolve(__dirname, settings.frontendFolder); const themeFolder = path.resolve(frontendFolder, settings.themeFolder); +const statsFolder = path.resolve(__dirname, settings.statsOutput); const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); -const addonFrontendFolder = path.resolve(__dirname, settings.addonFrontendFolder); +const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder); +const generatedFlowImportsFolder = path.resolve(__dirname, settings.generatedFlowImportsFolder); const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder); -const statsFile = path.resolve(frontendBundleFolder, '..', 'config', 'stats.json'); + +const statsFile = path.resolve(statsFolder, 'stats.json'); const projectStaticAssetsFolders = [ path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'), @@ -40,7 +43,7 @@ const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.reso const themeOptions = { devMode: false, - // The following matches folder 'target/flow-frontend/themes/' + // The following matches folder 'frontend/generated/themes/' // (not 'frontend/themes') for theme in JAR that is copied there themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder), themeProjectFolders: themeProjectFolders, @@ -182,6 +185,7 @@ function statsExtracterPlugin(): PluginOption { .sort() .filter((value, index, self) => self.indexOf(value) === index); + mkdirSync(path.dirname(statsFile), { recursive: true }); writeFileSync(statsFile, JSON.stringify({ npmModules }, null, 1)); } }; @@ -460,8 +464,7 @@ let spaMiddlewareForceRemoved = false; const allowedFrontendFolders = [ frontendFolder, - addonFrontendFolder, - path.resolve(addonFrontendFolder, '..', 'frontend'), // Contains only generated-flow-imports + path.resolve(generatedFlowImportsFolder), // Contains only generated-flow-imports path.resolve(__dirname, 'node_modules') ]; @@ -500,6 +503,7 @@ export const vaadinConfig: UserConfigFn = (env) => { base: '', resolve: { alias: { + '@vaadin/flow-frontend': jarResourcesFolder, Frontend: frontendFolder }, preserveSymlinks: true @@ -563,14 +567,14 @@ export const vaadinConfig: UserConfigFn = (env) => { ] }), { - name: 'vaadin:force-remove-spa-middleware', + name: 'vaadin:force-remove-html-middleware', transformIndexHtml: { enforce: 'pre', transform(_html, { server }) { if (server && !spaMiddlewareForceRemoved) { server.middlewares.stack = server.middlewares.stack.filter((mw) => { const handleName = '' + mw.handle; - return !handleName.includes('viteSpaFallbackMiddleware'); + return !handleName.includes('viteHtmlFallbackMiddleware'); }); spaMiddlewareForceRemoved = true; } diff --git a/runtime/src/main/java/com/vaadin/quarkus/EnableWebsockets.java b/runtime/src/main/java/com/vaadin/quarkus/EnableWebsockets.java index 061dd51..da19294 100644 --- a/runtime/src/main/java/com/vaadin/quarkus/EnableWebsockets.java +++ b/runtime/src/main/java/com/vaadin/quarkus/EnableWebsockets.java @@ -19,22 +19,39 @@ import jakarta.websocket.server.ServerApplicationConfig; import jakarta.websocket.server.ServerEndpointConfig; +import java.util.Collections; import java.util.Set; /** * Only purpose of this class is to automatically enable quarkus WebSocket * deployment, in order to make Atmosphere JSR365Endpoint work. + * + * Quarkus enables WebSocket deployment only if it finds annotated endpoints + * (@{@link jakarta.websocket.server.ServerEndpoint}) or implementors of + * {@link ServerApplicationConfig} interface. + * + * Unfortunately, if at least one implementation of + * {@link ServerApplicationConfig} is found, annotated endpoints are not + * deployed automatically. + * + * To circumvent this problem, implementation of + * {@link #getAnnotatedEndpointClasses(Set)} method will return all the provided + * scanned annotated endpoints. Although Javadocs says that the passed set of + * scanned classes contains all the annotated endpoint classes in the JAR or WAR + * file containing the implementation of this interface, Quarkus will instead + * provide all available annotated endpoints found at build time. + * */ public class EnableWebsockets implements ServerApplicationConfig { @Override public Set getEndpointConfigs( Set> endpointClasses) { - return null; + return Collections.emptySet(); } @Override public Set> getAnnotatedEndpointClasses(Set> scanned) { - return null; + return scanned; } }