diff --git a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java index 84bd32fa212..c745b56592a 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java @@ -686,6 +686,7 @@ protected final native AtmosphereConfiguration createConfig() fallbackTransport: 'long-polling', contentType: 'application/json; charset=UTF-8', reconnectInterval: 5000, + maxWebsocketErrorRetries: 12, timeout: -1, maxReconnectOnClose: 10000000, trackMessageLength: true, diff --git a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java index b918e60eb72..b023ecca5ee 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java @@ -458,6 +458,18 @@ public void pushOk(PushConnection pushConnection) { debug("pushOk()"); if (isReconnecting()) { resolveTemporaryError(Type.PUSH); + if (registry.getRequestResponseTracker().hasActiveRequest()) { + debug("pushOk() Reset active request state when reconnecting PUSH because of a network error."); + endRequest(); + // for bidirectional transport, the pending message is not sent + // as reconnection payload, so immediately push the pending + // changes on reconnect + if (pushConnection.isBidirectional()) { + Console.debug( + "Flush pending messages after PUSH reconnection."); + registry.getMessageSender().sendInvocationsToServer(); + } + } } } diff --git a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java index 80480c2dd4e..80e65ecf702 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java @@ -64,6 +64,8 @@ public enum ResynchronizationState { private ResynchronizationState resynchronizationState = ResynchronizationState.NOT_ACTIVE; + private JsonObject pushPendingMessage; + /** * Creates a new instance connected to the given registry. * @@ -104,6 +106,17 @@ public void sendInvocationsToServer() { * */ private void doSendInvocationsToServer() { + // If there's a stored message, resend it and postpone processing the + // rest of the queued messages to prevent resynchronization issues. + if (pushPendingMessage != null) { + Console.log("Sending pending push message " + + pushPendingMessage.toJson()); + JsonObject payload = pushPendingMessage; + pushPendingMessage = null; + registry.getRequestResponseTracker().startRequest(); + send(payload); + return; + } ServerRpcQueue serverRpcQueue = registry.getServerRpcQueue(); if (serverRpcQueue.isEmpty() @@ -181,6 +194,13 @@ private JsonObject preparePayload(final JsonArray reqInvocations, */ public void send(final JsonObject payload) { if (push != null && push.isBidirectional()) { + // When using bidirectional transport, the payload is not resent + // to the server during reconnection attempts. + // Keep a copy of the message, so that it could be resent to the + // server after a reconnection. + // Reference will be cleaned up once the server confirms it has + // seen this message + pushPendingMessage = payload; push.push(payload); } else { registry.getXhrConnection().send(payload); @@ -260,7 +280,14 @@ public void resynchronize() { */ public void setClientToServerMessageId(int nextExpectedId, boolean force) { if (nextExpectedId == clientToServerMessageId) { - // No op as everything matches they way it should + // Everything matches they way it should + // Remove potential pending PUSH message if it has already been seen + // by the server. + if (pushPendingMessage != null + && (int) pushPendingMessage.getNumber( + ApplicationConstants.CLIENT_TO_SERVER_ID) < nextExpectedId) { + pushPendingMessage = null; + } return; } if (force) { diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java index b6562f494ab..6248509201f 100644 --- a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java +++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java @@ -171,6 +171,9 @@ public class BuildDevBundleMojo extends AbstractMojo @Parameter(defaultValue = "${project.basedir}/src/main/" + FRONTEND) private File frontendDirectory; + @Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false") + private boolean npmExcludeWebComponents; + @Override public void execute() throws MojoFailureException { long start = System.nanoTime(); @@ -469,4 +472,9 @@ public boolean checkRuntimeDependency(String groupId, String artifactId, Consumer missingDependencyMessageConsumer) { return false; } + + @Override + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt index 977572330ad..84ea3e5add4 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt @@ -218,6 +218,8 @@ internal class GradlePluginAdapter( override fun applicationIdentifier(): String = config.applicationIdentifier.get() + override fun isNpmExcludeWebComponents(): Boolean = config.npmExcludeWebComponents.get() + override fun checkRuntimeDependency( groupId: String, artifactId: String, diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt index 79498ac60de..f5467c90f2e 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt @@ -142,6 +142,9 @@ internal class PrepareFrontendInputProperties(private val config: PluginEffectiv @Input public fun getApplicationIdentifier(): Provider = config.applicationIdentifier + @Input + public fun getNpmExcludeWebComponents(): Provider = config.npmExcludeWebComponents + @Input @Optional public fun getNodeExecutablePath(): Provider = tools diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt index 0924369e56c..1dfcf839177 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt @@ -282,6 +282,8 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val public abstract val applicationIdentifier: Property + public abstract val npmExcludeWebComponents: Property + public fun filterClasspath(@DelegatesTo(value = ClasspathFilter::class, strategy = Closure.DELEGATE_FIRST) block: Closure<*>) { block.delegate = classpathFilter block.resolveStrategy = Closure.DELEGATE_FIRST @@ -439,6 +441,9 @@ public class PluginEffectiveConfiguration( )) .overrideWithSystemProperty("vaadin.${InitParameters.APPLICATION_IDENTIFIER}") + public val npmExcludeWebComponents: Provider = extension + .npmExcludeWebComponents.convention(false) + /** * Finds the value of a boolean property. It searches in gradle and system properties. * @@ -499,7 +504,8 @@ public class PluginEffectiveConfiguration( "alwaysExecutePrepareFrontend=${alwaysExecutePrepareFrontend.get()}, " + "frontendHotdeploy=${frontendHotdeploy.get()}," + "reactEnable=${reactEnable.get()}," + - "cleanFrontendFiles=${cleanFrontendFiles.get()}" + + "cleanFrontendFiles=${cleanFrontendFiles.get()}," + + "npmExcludeWebComponents=${npmExcludeWebComponents.get()}" + ")" public companion object { public fun get(project: Project): PluginEffectiveConfiguration = diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java index ae3b389c61f..62bc3d37fc0 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java @@ -234,6 +234,9 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo @Parameter(property = InitParameters.REACT_ENABLE, defaultValue = "${null}") private Boolean reactEnable; + @Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false") + private boolean npmExcludeWebComponents; + /** * Identifier for the application. *

@@ -570,4 +573,9 @@ public String applicationIdentifier() { project.getGroupId() + ":" + project.getArtifactId(), StandardCharsets.UTF_8); } + + @Override + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } } diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java index 479e6cbbf48..5926cc68f68 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java @@ -176,7 +176,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { .withHomeNodeExecRequired(requireHomeNodeExec()) .setJavaResourceFolder(javaResourceFolder()) .withProductionMode(productionMode) - .withReact(isReactEnabled()); + .withReact(isReactEnabled()) + .withNpmExcludeWebComponents( + isNpmExcludeWebComponents()); new NodeTasks(options).execute(); logInfo("SBOM generation created node_modules and all needed metadata. " + "If you don't need it, please run mvn vaadin:clean-frontend"); diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java index afd1511c7cb..dd8c5679207 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java @@ -77,6 +77,7 @@ import static com.vaadin.flow.server.InitParameters.FRONTEND_HOTDEPLOY; import static com.vaadin.flow.server.InitParameters.NODE_DOWNLOAD_ROOT; import static com.vaadin.flow.server.InitParameters.NODE_VERSION; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_INITIAL_UIDL; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE; @@ -165,7 +166,9 @@ public static void prepareFrontend(PluginAdapterBase adapter) .setNodeAutoUpdate(adapter.nodeAutoUpdate()) .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .setJavaResourceFolder(adapter.javaResourceFolder()) - .withProductionMode(false).withReact(adapter.isReactEnabled()); + .withProductionMode(false).withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); // Copy jar artifact contents in TaskCopyFrontendFiles options.copyResources(adapter.getJarFiles()); @@ -263,6 +266,10 @@ public static File propagateBuildInfo(PluginAdapterBase adapter) { } buildInfo.put(REACT_ENABLE, adapter.isReactEnabled()); + if (adapter.isNpmExcludeWebComponents()) { + buildInfo.put(NPM_EXCLUDE_WEB_COMPONENTS, + adapter.isNpmExcludeWebComponents()); + } try { FileUtils.forceMkdir(token.getParentFile()); @@ -339,7 +346,9 @@ public static void runNodeUpdater(PluginAdapterBuild adapter) .withPostinstallPackages(adapter.postinstallPackages()) .withCiBuild(adapter.ciBuild()) .withForceProductionBuild(adapter.forceProductionBuild()) - .withReact(adapter.isReactEnabled()); + .withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); new NodeTasks(options).execute(); } catch (ExecutionFailedException exception) { throw exception; @@ -405,7 +414,9 @@ public static void runDevBuildNodeUpdater(PluginAdapterBuild adapter) .withBundleBuild(true) .skipDevBundleBuild(adapter.skipDevBundleBuild()) .withCompressBundle(adapter.compressBundle()) - .withReact(adapter.isReactEnabled()); + .withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); new NodeTasks(options).execute(); } catch (ExecutionFailedException exception) { throw exception; @@ -751,6 +762,7 @@ public static void updateBuildFile(PluginAdapterBuild adapter, buildInfo.remove(Constants.CONNECT_OPEN_API_FILE_TOKEN); buildInfo.remove(Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN); buildInfo.remove(InitParameters.BUILD_FOLDER); + buildInfo.remove(InitParameters.NPM_EXCLUDE_WEB_COMPONENTS); // Premium features flag is always true, because Vaadin CI server // uses Enterprise sub, thus it's always true. // Thus, resets the premium feature flag and DAU flag before asking diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java index 2a3186651ad..4c431d0b4a7 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java @@ -337,4 +337,11 @@ default Lookup createLookup(ClassFinder classFinder) { * {@literal blank}. */ String applicationIdentifier(); + + /** + * Whether to include web component npm packages in packages.json. + * + * @return {@code true} to include web component npm packages. + */ + boolean isNpmExcludeWebComponents(); } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Component.java b/flow-server/src/main/java/com/vaadin/flow/component/Component.java index a68d6895e8d..854a7ea97eb 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Component.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Component.java @@ -15,10 +15,15 @@ */ package com.vaadin.flow.component; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import java.util.stream.Stream.Builder; @@ -32,6 +37,7 @@ import com.vaadin.flow.dom.ShadowRoot; import com.vaadin.flow.i18n.I18NProvider; import com.vaadin.flow.internal.AnnotationReader; +import com.vaadin.flow.internal.CurrentInstance; import com.vaadin.flow.internal.LocaleUtil; import com.vaadin.flow.internal.nodefeature.ElementData; import com.vaadin.flow.server.Attributes; @@ -820,4 +826,38 @@ public void removeFromParent() { getElement().removeFromParent(); } + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + if (this instanceof UI ui) { + Map, CurrentInstance> instances = CurrentInstance + .setCurrent(ui); + try { + out.defaultWriteObject(); + } finally { + CurrentInstance.restoreInstances(instances); + } + } else { + out.defaultWriteObject(); + } + } + + @Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + if (this instanceof UI ui) { + Map, CurrentInstance> instances = CurrentInstance + .getInstances(); + // Cannot use CurrentInstance.setCurrent(this) because it will try + // to get VaadinSession from UI.internals that is not yet available + CurrentInstance.set(UI.class, ui); + try { + in.defaultReadObject(); + } finally { + CurrentInstance.restoreInstances(instances); + } + } else { + in.defaultReadObject(); + } + } + } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java index a9889197a45..6f0a8df3bc0 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java @@ -285,4 +285,9 @@ public class InitParameters implements Serializable { */ public static final String APPLICATION_IDENTIFIER = "applicationIdentifier"; + /** + * Configuration name for excluding npm packages for web components. + */ + public static final String NPM_EXCLUDE_WEB_COMPONENTS = "npm.excludeWebComponents"; + } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java index b1170eb84ff..09dacdb03d8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java @@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; + import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; @@ -1097,6 +1098,14 @@ private void readObject(ObjectInputStream stream) Map, CurrentInstance> old = CurrentInstance.setCurrent(this); try { stream.defaultReadObject(); + // Add-ons may have Listener classes that nullify themselves during + // serialization (e.g. Collaboration Kit) and restore instances in + // some custom way later on. + // Removing null elements prevents application to fail if restore + // actions are not applied eagerly + requestHandlers.remove(null); + destroyListeners.remove(null); + uIs = (Map) stream.readObject(); resourceRegistry = (StreamResourceRegistry) stream.readObject(); pendingAccessQueue = new ConcurrentLinkedQueue<>(); @@ -1107,27 +1116,33 @@ private void readObject(ObjectInputStream stream) private void writeObject(java.io.ObjectOutputStream stream) throws IOException { - boolean serializeUIs = true; - - // If service is null it has just been deserialized and should be - // serialized in - // the same way again - if (getService() != null) { - ApplicationConfiguration appConfiguration = ApplicationConfiguration - .get(getService().getContext()); - if (!appConfiguration.isProductionMode() && !appConfiguration - .isDevModeSessionSerializationEnabled()) { - serializeUIs = false; + Map, CurrentInstance> instanceMap = CurrentInstance + .setCurrent(this); + try { + boolean serializeUIs = true; + + // If service is null it has just been deserialized and should be + // serialized in + // the same way again + if (getService() != null) { + ApplicationConfiguration appConfiguration = ApplicationConfiguration + .get(getService().getContext()); + if (!appConfiguration.isProductionMode() && !appConfiguration + .isDevModeSessionSerializationEnabled()) { + serializeUIs = false; + } } - } - stream.defaultWriteObject(); - if (serializeUIs) { - stream.writeObject(uIs); - stream.writeObject(resourceRegistry); - } else { - stream.writeObject(new HashMap<>()); - stream.writeObject(new StreamResourceRegistry(this)); + stream.defaultWriteObject(); + if (serializeUIs) { + stream.writeObject(uIs); + stream.writeObject(resourceRegistry); + } else { + stream.writeObject(new HashMap<>()); + stream.writeObject(new StreamResourceRegistry(this)); + } + } finally { + CurrentInstance.restoreInstances(instanceMap); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java index 4c7a335a3b3..ac1542d1fc7 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java @@ -316,7 +316,8 @@ public void execute() { Map filteredApplicationDependencies = new ExclusionFilter( options.getClassFinder(), options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)) + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()) .exclude(applicationDependencies); // Add application dependencies diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java index 361fe37402b..5ae30bb629f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java @@ -43,6 +43,8 @@ public class ExclusionFilter implements Serializable { private final boolean reactEnabled; + private final boolean excludeWebComponentNpmPackages; + /** * Create a new exclusion filter. * @@ -52,8 +54,24 @@ public class ExclusionFilter implements Serializable { * whether React is enabled */ public ExclusionFilter(ClassFinder finder, boolean reactEnabled) { + this(finder, reactEnabled, false); + } + + /** + * Create a new exclusion filter. + * + * @param finder + * the class finder to use + * @param reactEnabled + * whether React is enabled + * @param excludeWebComponentNpmPackages + * whether to exclude web component npm packages + */ + public ExclusionFilter(ClassFinder finder, boolean reactEnabled, + boolean excludeWebComponentNpmPackages) { this.finder = finder; this.reactEnabled = reactEnabled; + this.excludeWebComponentNpmPackages = excludeWebComponentNpmPackages; } /** @@ -95,7 +113,7 @@ private Set getExclusions(URL versionsResource) throws IOException { VersionsJsonConverter convert = new VersionsJsonConverter( Json.parse( IOUtils.toString(content, StandardCharsets.UTF_8)), - reactEnabled); + reactEnabled, excludeWebComponentNpmPackages); return convert.getExclusions(); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java index 1f2535feaca..21f6f0fa1f6 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java @@ -177,7 +177,8 @@ private JsonObject getFilteredVersionsFromResource(URL versionsResource, Json.parse( IOUtils.toString(content, StandardCharsets.UTF_8)), options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)); + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()); versionsJson = convert.getConvertedJson(); versionsJson = new VersionsJsonFilter(getPackageJson(), DEPENDENCIES) @@ -617,6 +618,10 @@ private void putHillaComponentsDependencies( if (options.isReactEnabled()) { dependencies.putAll(readDependenciesIfAvailable( "hilla/components/react", packageJsonKey)); + if (options.isNpmExcludeWebComponents()) { + // remove dependencies that depends on web components + dependencies.remove("@vaadin/hilla-react-crud"); + } } else { dependencies.putAll(readDependenciesIfAvailable( "hilla/components/lit", packageJsonKey)); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java index db72b6a31bb..124bb597954 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java @@ -125,6 +125,8 @@ public class Options implements Serializable { private boolean reactEnable = true; + private boolean npmExcludeWebComponents = false; + /** * Removes generated files from a previous execution that are no more * created. @@ -967,4 +969,25 @@ public Options withCleanOldGeneratedFiles(boolean clean) { public boolean isCleanOldGeneratedFiles() { return cleanOldGeneratedFiles; } + + /** + * Sets whether to exclude web component npm packages in packages.json. + * + * @return this builder + */ + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } + + /** + * Sets whether to exclude web component npm packages in packages.json. + * + * @param exclude + * whether to exclude web component npm packages + * @return this builder + */ + public Options withNpmExcludeWebComponents(boolean exclude) { + this.npmExcludeWebComponents = exclude; + return this; + } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java index 6017d5ceb55..94d157159a2 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java @@ -217,7 +217,8 @@ private boolean updatePackageJsonDependencies(JsonObject packageJson, Map filteredApplicationDependencies = new ExclusionFilter( finder, options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)) + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()) .exclude(applicationDependencies); // Add application dependencies diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java index bfeb9afacd0..bd2219d8928 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java @@ -42,6 +42,7 @@ class VersionsJsonConverter { static final String VAADIN_CORE_NPM_PACKAGE = "@vaadin/vaadin-core"; + static final String VAADIN_BUNDLES = "@vaadin/bundles"; private static final String JS_VERSION = "jsVersion"; private static final String NPM_NAME = "npmName"; private static final String NPM_VERSION = "npmVersion"; @@ -74,15 +75,18 @@ class VersionsJsonConverter { private boolean reactEnabled; + private boolean excludeWebComponents; + private Set exclusions; private static Logger getLogger() { return LoggerFactory.getLogger(VersionsJsonConverter.class); } - VersionsJsonConverter(JsonObject platformVersions, - boolean collectReactComponents) { - this.reactEnabled = collectReactComponents; + VersionsJsonConverter(JsonObject platformVersions, boolean reactEnabled, + boolean excludeWebComponents) { + this.reactEnabled = reactEnabled; + this.excludeWebComponents = excludeWebComponents; exclusions = new HashSet<>(); convertedObject = Json.createObject(); @@ -135,6 +139,8 @@ private void excludeDependencies() { private boolean isIncludedByMode(String mode) { if (mode == null || mode.isBlank() || MODE_ALL.equalsIgnoreCase(mode)) { return true; + } else if (excludeWebComponents) { + return false; } else if (reactEnabled) { return MODE_REACT.equalsIgnoreCase(mode); } else { @@ -151,7 +157,19 @@ private void addDependency(JsonObject obj) { if (Objects.equals(npmName, VAADIN_CORE_NPM_PACKAGE)) { return; } + if (excludeWebComponents && Objects.equals(npmName, VAADIN_BUNDLES)) { + exclusions.add(npmName); + return; + } if (!isIncludedByMode(mode)) { + if (excludeWebComponents) { + // collecting exclusions also from non-included dependencies + // with a mode (react), when web components are not wanted. + if (MODE_REACT.equalsIgnoreCase(mode)) { + exclusions.add(npmName); + } + collectExclusions(obj); + } return; } if (obj.hasKey(NPM_VERSION)) { @@ -166,6 +184,12 @@ private void addDependency(JsonObject obj) { } convertedObject.put(npmName, version); + collectExclusions(obj); + getLogger().debug("versions.json adds dependency {} with version {}{}", + npmName, version, (mode != null ? " for mode " + mode : "")); + } + + private void collectExclusions(JsonObject obj) { if (obj.hasKey(EXCLUSIONS)) { JsonArray array = obj.getArray(EXCLUSIONS); if (array != null) { @@ -173,8 +197,6 @@ private void addDependency(JsonObject obj) { .forEach(i -> exclusions.add(array.getString(i))); } } - getLogger().debug("versions.json adds dependency {} with version {}{}", - npmName, version, (mode != null ? " for mode " + mode : "")); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java index 5063bbbc1d9..5c7b44a2381 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java @@ -52,6 +52,7 @@ import static com.vaadin.flow.server.InitParameters.FRONTEND_HOTDEPLOY; import static com.vaadin.flow.server.InitParameters.NODE_DOWNLOAD_ROOT; import static com.vaadin.flow.server.InitParameters.NODE_VERSION; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_ENABLE_DEV_SERVER; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_INITIAL_UIDL; @@ -187,6 +188,11 @@ protected Map getConfigParametersUsingTokenData( String.valueOf(buildInfo.getBoolean(PREMIUM_FEATURES))); } + if (buildInfo.hasKey(NPM_EXCLUDE_WEB_COMPONENTS)) { + params.put(NPM_EXCLUDE_WEB_COMPONENTS, String + .valueOf(buildInfo.getBoolean(NPM_EXCLUDE_WEB_COMPONENTS))); + } + setDevModePropertiesUsingTokenData(params, buildInfo); return params; } diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts index e40c8e972b4..e51c0f57df4 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts @@ -149,9 +149,7 @@ self.addEventListener('message', (event) => { self.addEventListener('push', (e) => { const data = e.data?.json(); if (data) { - self.registration.showNotification(data.title, { - body: data.body, - }); + self.registration.showNotification(data.title, data.options); } }); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java index 0cef96601bf..8bfa41a1c36 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java @@ -470,6 +470,49 @@ public void testGetPlatformPinnedDependencies_reactNotAvailable_noReactComponent @Test public void testGetPlatformPinnedDependencies_reactAvailable_containsReactComponents() throws IOException, ClassNotFoundException { + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertTrue( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + @Test + public void testGetPlatformPinnedDependencies_reactAvailable_excludeWebComponents() + throws IOException, ClassNotFoundException { + options.withNpmExcludeWebComponents(true); + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + // @vaadin/button doesn't have 'mode' set, so it should be included + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertFalse(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertFalse( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + @Test + public void testGetPlatformPinnedDependencies_reactDisabled_excludeWebComponents() + throws IOException, ClassNotFoundException { + options.withReact(false); + options.withNpmExcludeWebComponents(true); + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + // @vaadin/button doesn't have 'mode' set, so it should be included + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertFalse(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertFalse( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + private void generateTestDataForReactComponents() + throws IOException, ClassNotFoundException { File coreVersionsFile = File.createTempFile("vaadin-core-versions", ".json", temporaryFolder.newFolder()); File vaadinVersionsFile = File.createTempFile("vaadin-versions", @@ -514,13 +557,6 @@ public void testGetPlatformPinnedDependencies_reactAvailable_containsReactCompon Class clazz = FeatureFlags.class; // actual class doesn't matter Mockito.doReturn(clazz).when(finder).loadClass( "com.vaadin.flow.component.react.ReactAdapterComponent"); - - JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); - - Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); - Assert.assertTrue(pinnedVersions.hasKey("@vaadin/react-components")); - Assert.assertTrue( - pinnedVersions.hasKey("@vaadin/react-components-pro")); } @Test diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java index 196cf990dca..298aabb27e2 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java @@ -23,6 +23,8 @@ import static com.vaadin.flow.server.frontend.NodeUpdater.OVERRIDES; import static com.vaadin.flow.server.frontend.NodeUpdater.VAADIN_DEP_KEY; import static com.vaadin.flow.server.frontend.VersionsJsonConverter.VAADIN_CORE_NPM_PACKAGE; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import java.io.File; import java.io.IOException; @@ -50,7 +52,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vaadin.flow.di.Lookup; import com.vaadin.flow.server.Constants; import com.vaadin.flow.server.frontend.scanner.ClassFinder; import com.vaadin.flow.server.frontend.scanner.FrontendDependencies; @@ -848,6 +849,163 @@ public void reactDisabled_coreDependenciesAdded() throws IOException { } + @Test + public void webComponentsExcluded_reactDisabled_noExclusionsInVersions() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(false) + .withNpmExcludeWebComponents(true); + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertTrue(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactDisabled_exclusionsInVersions_noWebComponentsIncluded() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION, + Set.of(VAADIN_DIALOG)); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(false) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactEnabled_noExclusionsInVersions() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(true) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertTrue(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactEnabled_exclusionsInVersions_noWebComponentsIncluded() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION, + Set.of(VAADIN_DIALOG)); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(true) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + private void execTaskUpdatePackages( + Map scannedApplicationDependencies, + Options options) { + final FrontendDependencies frontendDependenciesScanner = Mockito + .mock(FrontendDependencies.class); + Mockito.when(frontendDependenciesScanner.getPackages()) + .thenReturn(scannedApplicationDependencies); + final TaskUpdatePackages task = new TaskUpdatePackages( + frontendDependenciesScanner, options) { + }; + task.execute(); + } + + private boolean hasInDependencies(JsonObject newPackageJson, String key) { + return newPackageJson.hasKey("dependencies") + && newPackageJson.getObject("dependencies").hasKey(key); + } + + private boolean hasInVaadinDependencies(JsonObject newPackageJson, + String key) { + return newPackageJson.hasKey("vaadin") && newPackageJson + .getObject("vaadin").getObject("dependencies").hasKey(key); + } + private void createBasicVaadinVersionsJson() { createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java index 4a9a3aecfc6..f5b694c66fb 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java @@ -64,7 +64,7 @@ public void convertPlatformVersions() throws IOException { // @formatter:on VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), false); + Json.parse(json), false, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -127,7 +127,7 @@ public void reactRouterInUse_reactComponentsAreAdded() { """.formatted(VAADIN_CORE_NPM_PACKAGE); VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, true); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -202,7 +202,7 @@ public void reactRouterNotUsed_reactComponentsIgnored() { """.formatted(VAADIN_CORE_NPM_PACKAGE); VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), false); + Json.parse(json), false, true); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -278,7 +278,7 @@ public void testModeProperty() { // react enabled VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -295,8 +295,27 @@ public void testModeProperty() { Assert.assertFalse(convertedJson.hasKey("react-components")); Assert.assertFalse(convertedJson.hasKey("react-components-pro")); + // react enabled, exclude web components + convert = new VersionsJsonConverter(Json.parse(json), true, true); + convertedJson = convert.getConvertedJson(); + Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); + Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); + Assert.assertTrue(convertedJson.hasKey("@polymer/iron-list")); + Assert.assertFalse( + convertedJson.hasKey("@vaadin/react-components-pro")); + Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + + Assert.assertFalse(convertedJson.hasKey("flow")); + Assert.assertFalse(convertedJson.hasKey("core")); + Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); + Assert.assertFalse(convertedJson.hasKey("platform")); + Assert.assertFalse(convertedJson.hasKey("react")); + Assert.assertFalse(convertedJson.hasKey("react-pro")); + Assert.assertFalse(convertedJson.hasKey("react-components")); + Assert.assertFalse(convertedJson.hasKey("react-components-pro")); + // react disabled - convert = new VersionsJsonConverter(Json.parse(json), false); + convert = new VersionsJsonConverter(Json.parse(json), false, false); convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -305,6 +324,24 @@ public void testModeProperty() { convertedJson.hasKey("@vaadin/react-components-pro")); Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + Assert.assertFalse(convertedJson.hasKey("flow")); + Assert.assertFalse(convertedJson.hasKey("core")); + Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); + Assert.assertFalse(convertedJson.hasKey("platform")); + Assert.assertFalse(convertedJson.hasKey("react")); + Assert.assertFalse(convertedJson.hasKey("react-pro")); + Assert.assertFalse(convertedJson.hasKey("react-components")); + + // react disabled, exclude web components + convert = new VersionsJsonConverter(Json.parse(json), false, true); + convertedJson = convert.getConvertedJson(); + Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); + Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); + Assert.assertTrue(convertedJson.hasKey("@polymer/iron-list")); + Assert.assertFalse( + convertedJson.hasKey("@vaadin/react-components-pro")); + Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + Assert.assertFalse(convertedJson.hasKey("flow")); Assert.assertFalse(convertedJson.hasKey("core")); Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); @@ -371,7 +408,7 @@ public void testExclusionsArrayProperty() { // react enabled VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -390,7 +427,7 @@ public void testExclusionsArrayProperty() { Assert.assertFalse(convertedJson.hasKey("react-components-pro")); // react disabled - convert = new VersionsJsonConverter(Json.parse(json), false); + convert = new VersionsJsonConverter(Json.parse(json), false, false); convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); diff --git a/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java b/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java index 57be740c9e1..e646a5d3b3f 100644 --- a/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java +++ b/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java @@ -5,19 +5,29 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.Serial; import java.io.Serializable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.UI; import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.server.RequestHandler; import com.vaadin.flow.server.StreamRegistration; import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.VaadinContext; import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.VaadinResponse; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinServletService; import com.vaadin.flow.server.VaadinSession; @@ -29,6 +39,30 @@ public class SerializationTest { + Runnable cleaner; + + @Before + public void enabledSerializationDebugInfo() { + String extendedDebugInfo = System + .getProperty("sun.io.serialization.extendedDebugInfo"); + System.setProperty("sun.io.serialization.extendedDebugInfo", "true"); + cleaner = () -> { + if (extendedDebugInfo != null) { + System.setProperty("sun.io.serialization.extendedDebugInfo", + extendedDebugInfo); + } else { + System.clearProperty("sun.io.serialization.extendedDebugInfo"); + } + }; + } + + @After + public void restore() { + if (cleaner != null) { + cleaner.run(); + } + } + @Test public void testSerializeVaadinSession_accessQueueIsRecreated() throws Exception { @@ -123,8 +157,119 @@ public void testSerializeVaadinSession_notProductionMode_canSerializeWithoutTran Assert.assertNull(againSerializedAndDeserializedSession.getService()); } + @Test + // Covers serialization of UI scoped beans, e.g. in Kubernetes Kit + // https://github.com/vaadin/flow/issues/19967 + // https://github.com/vaadin/kubernetes-kit/issues/140 + public void serializeUI_currentUI_availableDuringSerialization() + throws Exception { + VaadinSession deserializeSession = serializeAndDeserializeWithUI(true, + true, ui -> ui.add(new MyComponent())); + MyComponent deserializedComponent = deserializeSession.getUIs() + .iterator().next().getChildren() + .filter(MyComponent.class::isInstance) + .map(MyComponent.class::cast).findFirst() + .orElseThrow(() -> new AssertionError( + "Custom component has not been deserialized")); + + deserializedComponent.checker.assertInstancesAvailable(); + } + + @Test + // Covers serialization of UI scoped beans, e.g. in Kubernetes Kit + // https://github.com/vaadin/flow/issues/19967 + // https://github.com/vaadin/kubernetes-kit/issues/140 + public void serializeUI_currentVaadinSession_availableDuringSerialization() + throws Exception { + VaadinSession deserializeSession = serializeAndDeserializeWithUI(true, + true, + ui -> ui.getSession().addRequestHandler(new MyListener())); + + MyListener deserializedListener = deserializeSession + .getRequestHandlers().stream() + .filter(MyListener.class::isInstance) + .map(MyListener.class::cast).findFirst() + .orElseThrow(() -> new AssertionError( + "Session request listener has not been deserialized")); + + deserializedListener.checker.assertSessionAvailable(); + } + + private static class SerializationInstancesChecker implements Serializable { + private boolean uiAvailableOnRead = false; + private boolean sessionAvailableOnRead = false; + private boolean uiAvailableOnWrite = false; + private boolean sessionAvailableOnWrite = false; + + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + uiAvailableOnWrite = UI.getCurrent() != null; + sessionAvailableOnWrite = VaadinSession.getCurrent() != null; + out.defaultWriteObject(); + } + + @Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + uiAvailableOnRead = UI.getCurrent() != null; + sessionAvailableOnRead = VaadinSession.getCurrent() != null; + } + + void assertInstancesAvailable() { + assertUIAvailable(); + assertSessionAvailable(); + } + + void assertUIAvailable() { + Assert.assertTrue( + "Expecting serialization hook to be called with UI thread local set", + uiAvailableOnWrite); + Assert.assertTrue( + "Expecting deserialization hook to be called with UI thread local set", + uiAvailableOnRead); + } + + void assertSessionAvailable() { + Assert.assertTrue( + "Expecting serialization hook to be called with VaadinSession thread local set", + sessionAvailableOnWrite); + Assert.assertTrue( + "Expecting deserialization hook to be called with VaadinSession thread local set", + sessionAvailableOnRead); + } + + } + + @Tag("my-component") + private static class MyComponent extends Component { + + private final SerializationInstancesChecker checker = new SerializationInstancesChecker(); + + } + + private static class MyListener implements RequestHandler { + + private final SerializationInstancesChecker checker = new SerializationInstancesChecker(); + + @Override + public boolean handleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) + throws IOException { + return false; + } + } + + private static VaadinSession serializeAndDeserializeWithUI( + boolean serializeUI) throws Exception { + return serializeAndDeserializeWithUI(serializeUI, false, ui -> { + }); + } + private static VaadinSession serializeAndDeserializeWithUI( - boolean serializeUI) throws IOException, ClassNotFoundException { + boolean serializeUI, boolean background, Consumer uiConsumer) + throws Exception { + VaadinService vaadinService = new MockVaadinService(false, serializeUI); VaadinSession session = new VaadinSession(vaadinService); // This is done only for test purpose to init the session lock, @@ -136,13 +281,25 @@ private static VaadinSession serializeAndDeserializeWithUI( MockUI ui = new MockUI(session); ui.doInit(null, 42); session.addUI(ui); - - session = serializeAndDeserialize(session); + uiConsumer.accept(ui); + + VaadinSession deserializedSession; + if (background) { + deserializedSession = CompletableFuture.supplyAsync(() -> { + try { + return serializeAndDeserialize(session); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).get(); + } else { + deserializedSession = serializeAndDeserialize(session); + } // This is done only for test purpose to refresh the session lock, // should be called by Flow internally as soon as the session has // been retrieved from http session. - session.refreshTransients(null, vaadinService); - return session; + deserializedSession.refreshTransients(null, vaadinService); + return deserializedSession; } private static S serializeAndDeserialize(S s) diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java new file mode 100644 index 00000000000..4849a8eff27 --- /dev/null +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java @@ -0,0 +1,6 @@ +package com.vaadin.flow.webpush; + +import java.io.Serializable; + +public record WebPushAction(String action, String title, String icon) implements Serializable { +} diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java new file mode 100644 index 00000000000..fdc0597eafd --- /dev/null +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java @@ -0,0 +1,20 @@ +package com.vaadin.flow.webpush; + +import java.io.Serializable; +import java.util.List; + +public record WebPushOptions(List actions, + String badge, + String body, + Serializable data, + String dir, + String icon, + String image, + String lang, + boolean renotify, + boolean requireInteraction, + boolean silent, + String tag, + long timestamp, + List vibrate) implements Serializable { +} diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java index bb72be89f9f..c9ae1667276 100644 --- a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java @@ -16,6 +16,8 @@ package com.vaadin.flow.webpush; +import java.util.List; + import nl.martijndwars.webpush.Subscription; import com.vaadin.flow.component.Text; @@ -44,6 +46,12 @@ public class WebPushView extends Div { WebPush webPush; private final Div log; + private final WebPushAction webPushAction = new WebPushAction( + "dashboard", + "Open Dashboard", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png" + ); + private Subscription subscription; public WebPushView() { @@ -73,8 +81,25 @@ public WebPushView() { notify = new NativeButton("Notify", event -> { if (subscription != null) { + WebPushOptions webPushOptions = new WebPushOptions( + List.of(webPushAction), + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "Testing notification", + "This is my data!", + "rtl", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "de-DE", + true, + true, + false, + "My Notification", + System.currentTimeMillis(), + List.of(500, 500, 500) + ); + webPush.sendNotification(subscription, - new WebPushMessage(TEST_TITLE, "Testing notification")); + new WebPushMessage(TEST_TITLE, webPushOptions)); addLogEntry("Sent notification"); } else { addLogEntry("No notification sent due to missing subscription"); diff --git a/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java b/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java index feedcfc4768..03aeeb4bd24 100644 --- a/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java +++ b/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java @@ -153,7 +153,18 @@ public boolean isNotificationPresent(WebDriver driver) { .then( (notifications) => { return notifications.length == 1 && notifications[0].title === 'Test title' && - notifications[0].body === 'Testing notification'; + notifications[0].body === 'Testing notification' && + notifications[0].badge === 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png' && + notifications[0].data === 'This is my data!' && + notifications[0].dir === 'rtl' && + notifications[0].icon === 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png' && + notifications[0].lang === 'de-DE' && + notifications[0].renotify === true && + notifications[0].requireInteraction === true && + notifications[0].silent === false && + notifications[0].tag === 'My Notification' && + Array.isArray(notifications[0].actions) && notifications[0].actions.length > 0 && notifications[0].actions[0].action === 'dashboard' && + Array.isArray(notifications[0].vibrate) && notifications[0].vibrate.length > 0 && notifications[0].vibrate[0] === 500; }); """); } diff --git a/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java b/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java index 42ffbf2712a..01474ead43a 100644 --- a/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java +++ b/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java @@ -17,8 +17,8 @@ import java.io.Serializable; -import elemental.json.Json; -import elemental.json.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; /** * Web Push message object containing an information to be shown in the @@ -26,15 +26,40 @@ * * @since 24.2 */ -public record WebPushMessage(String title, String body) implements Serializable { +public record WebPushMessage(String title, ObjectNode options) implements Serializable { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Creates a new Web Push notification message with the specified title and various options + * fetched from a given Java object. + * + * @param title the notification title + * @param options any {@code Serializable} Java object representing custom settings to apply to the notification + * @see + * showNotification parameters + */ + public WebPushMessage(String title, Serializable options) { + this(title, objectMapper.convertValue(options, ObjectNode.class)); + } + + /** + * Creates a new Web Push notification message with just a title and body. + * + * @param title notification title + * @param body notification body + */ + public WebPushMessage(String title, String body) { + this(title, getBodyOption(body)); + } /** - * Creates a new Web Push notification message with title and body. + * Creates a new Web Push notification message with just a title. * * @param title notification title - * @param body notification body */ - public WebPushMessage { + public WebPushMessage(String title) { + this(title, (ObjectNode) null); } @Override @@ -48,9 +73,19 @@ public String toString() { * @return JSON representation of this message */ public String toJson() { - JsonObject json = Json.createObject(); + ObjectNode json = objectMapper.createObjectNode(); json.put("title", title); - json.put("body", body); - return json.toJson(); + if (options != null) { + json.set("options", options); + } + return json.toString(); + } + + private static ObjectNode getBodyOption(String body) { + ObjectNode objectNode = objectMapper.createObjectNode(); + if (body != null) { + objectNode.put("body", body); + } + return objectNode; } } diff --git a/pom.xml b/pom.xml index 142097c86b2..13ad6b6ef8f 100644 --- a/pom.xml +++ b/pom.xml @@ -107,7 +107,7 @@ 3.4.0 1.6.0 9.3.5 - 12.0.14 + 12.0.15 1.2.1 3.6.0 3.9.9 diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java index 67b2bc729b9..02665d9c1fc 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java @@ -81,6 +81,7 @@ import static com.vaadin.flow.server.Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN; import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES; import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE; import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED; @@ -270,6 +271,8 @@ public static DevModeHandler initDevModeHandler(Set> classes, boolean reactEnable = config.getBooleanProperty(REACT_ENABLE, FrontendUtils .isReactRouterRequired(options.getFrontendDirectory())); + boolean npmExcludeWebComponents = config + .getBooleanProperty(NPM_EXCLUDE_WEB_COMPONENTS, false); options.enablePackagesUpdate(true) .useByteCodeScanner(useByteCodeScanner) .withFrontendGeneratedFolder(frontendGeneratedFolder) @@ -289,7 +292,8 @@ public static DevModeHandler initDevModeHandler(Set> classes, .withFrontendHotdeploy( mode == Mode.DEVELOPMENT_FRONTEND_LIVERELOAD) .withBundleBuild(mode == Mode.DEVELOPMENT_BUNDLE) - .withReact(reactEnable); + .withReact(reactEnable) + .withNpmExcludeWebComponents(npmExcludeWebComponents); NodeTasks tasks = new NodeTasks(options); diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java index 5b4aa8eff9b..3d3da7bb10e 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java @@ -91,8 +91,10 @@ public void onOpen(WebSocket webSocket) { "Should not have been blocked too long after connection (elapsed time: " + elapsedTime + ")", elapsedTime < 1000); - viteConnection.close(); - closeLatch.await(2, TimeUnit.SECONDS); + if (!closeLatch.await(500, TimeUnit.MILLISECONDS)) { + viteConnection.close(); + closeLatch.await(500, TimeUnit.MILLISECONDS); + } } @Test diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java index 74e1c1d3c7a..fe077909642 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java @@ -149,11 +149,11 @@ private T execute(Supplier supplier) { if (session.hasLock()) { return supplier.get(); } else { - session.lock(); + session.getLockInstance().lock(); try { return supplier.get(); } finally { - session.unlock(); + session.getLockInstance().unlock(); } } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java index c179f339df8..ae57e1bcb58 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java @@ -15,8 +15,6 @@ */ package com.vaadin.flow.spring.scopes; -import jakarta.servlet.ServletContext; - import java.io.Serializable; import java.util.Collections; import java.util.HashMap; @@ -26,11 +24,10 @@ import java.util.Set; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.context.ApplicationContext; import org.springframework.lang.NonNull; -import org.springframework.web.context.support.WebApplicationContextUtils; import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.ComponentUtil; @@ -45,9 +42,6 @@ import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.server.UIInitEvent; import com.vaadin.flow.server.UIInitListener; -import com.vaadin.flow.server.VaadinContext; -import com.vaadin.flow.server.VaadinService; -import com.vaadin.flow.server.VaadinServletContext; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; import com.vaadin.flow.spring.annotation.RouteScopeOwner; @@ -63,7 +57,7 @@ * @since * */ -public class VaadinRouteScope extends AbstractScope implements UIInitListener { +public class VaadinRouteScope extends AbstractScope { public static final String VAADIN_ROUTE_SCOPE_NAME = "vaadin-route"; @@ -141,7 +135,7 @@ private void destroy() { } - private class NavigationListener + private static class NavigationListener implements BeforeEnterListener, AfterNavigationListener, ComponentEventListener, Serializable { @@ -315,8 +309,8 @@ public void onComponentEvent(DetachEvent event) { @Override protected Object doGet(String name, ObjectFactory objectFactory) { - RouteScopeOwner owner = getContext().findAnnotationOnBean(name, - RouteScopeOwner.class); + RouteScopeObjectFactory cast = (RouteScopeObjectFactory) objectFactory; + RouteScopeOwner owner = cast.getOwner(); if (!getNavigationListener().hasNavigationOwner(owner)) { assert owner != null; throw new IllegalStateException(String.format( @@ -324,15 +318,18 @@ protected Object doGet(String name, ObjectFactory objectFactory) { + "active navigation components chain: the scope defined by the bean '%s' doesn't exist.", owner.value(), name)); } - return super.doGet(name, objectFactory); + Object object = super.doGet(name, objectFactory); + if (object instanceof ObjectWithOwner wrapper) { + return wrapper.object; + } + return object; } @Override protected void storeBean(String name, Object bean) { - super.storeBean(name, bean); - RouteScopeOwner owner = getContext().findAnnotationOnBean(name, - RouteScopeOwner.class); - getNavigationListener().storeOwner(name, owner); + ObjectWithOwner wrapper = (ObjectWithOwner) bean; + super.storeBean(name, wrapper.object); + getNavigationListener().storeOwner(name, wrapper.owner); } BeanNamesWrapper getBeanNamesWrapper() { @@ -348,17 +345,6 @@ private boolean resetUI() { return true; } - @NonNull - private ApplicationContext getContext() { - VaadinService service = currentUI.getSession().getService(); - VaadinContext context = service.getContext(); - ServletContext servletContext = ((VaadinServletContext) context) - .getContext(); - assert servletContext != null; - return WebApplicationContextUtils - .getRequiredWebApplicationContext(servletContext); - } - @NonNull private NavigationListener getNavigationListener() { NavigationListener navigationListener = ComponentUtil @@ -369,10 +355,52 @@ private NavigationListener getNavigationListener() { } + private record ObjectWithOwner(Object object, RouteScopeOwner owner) { + } + + private static class RouteScopeObjectFactory + implements ObjectFactory { + + private final ObjectFactory objectFactory; + private final RouteScopeOwner owner; + + public RouteScopeObjectFactory(ObjectFactory objectFactory, + RouteScopeOwner owner) { + this.objectFactory = objectFactory; + this.owner = owner; + } + + @Override + public ObjectWithOwner getObject() throws BeansException { + return new ObjectWithOwner(objectFactory.getObject(), owner); + } + + public RouteScopeOwner getOwner() { + return owner; + } + } + + static class NavigationListenerRegistrar implements UIInitListener { + + @Override + public void uiInit(UIInitEvent event) { + NavigationListener listener = new NavigationListener(event.getUI()); + ComponentUtil.setData(event.getUI(), NavigationListener.class, + listener); + } + + } + + private ConfigurableListableBeanFactory beanFactory; + @Override public void postProcessBeanFactory( ConfigurableListableBeanFactory beanFactory) { beanFactory.registerScope(VAADIN_ROUTE_SCOPE_NAME, this); + beanFactory.registerSingleton( + NavigationListenerRegistrar.class.getName(), + new NavigationListenerRegistrar()); + this.beanFactory = beanFactory; } @Override @@ -381,16 +409,15 @@ public String getConversationId() { } @Override - public void uiInit(UIInitEvent event) { - NavigationListener listener = new NavigationListener(event.getUI()); - ComponentUtil.setData(event.getUI(), NavigationListener.class, - listener); + public Object get(String name, ObjectFactory objectFactory) { + return super.get(name, new RouteScopeObjectFactory(objectFactory, + beanFactory.findAnnotationOnBean(name, RouteScopeOwner.class))); } @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { BeanStore store = getBeanStoreIfExists(session); if (store == null) { @@ -400,11 +427,11 @@ protected BeanStore getBeanStore() { } return store; } finally { - session.unlock(); + session.getLockInstance().unlock(); } } - private RouteBeanStore getBeanStoreIfExists(VaadinSession session) { + private static RouteBeanStore getBeanStoreIfExists(VaadinSession session) { assert session.hasLock(); RouteStoreWrapper wrapper = session .getAttribute(RouteStoreWrapper.class); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java index a4893aaa204..5d5aa11e602 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.shared.Registration; /** * Implementation of Spring's @@ -67,7 +66,7 @@ public String getConversationId() { @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { BeanStore beanStore = session.getAttribute(BeanStore.class); if (beanStore == null) { @@ -76,7 +75,7 @@ protected BeanStore getBeanStore() { } return beanStore; } finally { - session.unlock(); + session.getLockInstance().unlock(); } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java index bfc11b79203..9e86020f460 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java @@ -108,7 +108,7 @@ public String getConversationId() { @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { UIStoreWrapper wrapper = session.getAttribute(UIStoreWrapper.class); if (wrapper == null) { @@ -117,7 +117,7 @@ protected BeanStore getBeanStore() { } return wrapper.getBeanStore(getUI()); } finally { - session.unlock(); + session.getLockInstance().unlock(); } } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index a0eb5d846d9..886073b9e75 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -97,6 +97,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor\\$Marker", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinHintsRegistrar", + "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinRouteScope(\\$.*)?", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinSessionScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java index 75d91b6cfaf..7622f9000d7 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java @@ -180,6 +180,8 @@ protected VaadinSession mockSession() { when(session.getConfiguration()).thenReturn(config); VaadinSession.setCurrent(session); + ReentrantLock lock = new ReentrantLock(); + when(session.getLockInstance()).thenReturn(lock); when(session.hasLock()).thenReturn(true); // keep a reference to the session so that it cannot be GCed. diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java index 3ed0babf774..d124ea822ac 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.web.context.WebApplicationContext; import com.vaadin.flow.component.Component; @@ -57,7 +58,10 @@ public static class AnotherNavigationTarget extends Component { @Override protected VaadinRouteScope getScope() { - return new VaadinRouteScope(); + VaadinRouteScope scope = new VaadinRouteScope(); + scope.postProcessBeanFactory( + Mockito.mock(ConfigurableListableBeanFactory.class)); + return scope; } @Test @@ -148,7 +152,8 @@ public void refresh_uiWithTheSameWindowName_beanInScopeIsDestroyedAfterRefresh() AtomicInteger count = new AtomicInteger(); scope.registerDestructionCallback("foo", () -> count.getAndIncrement()); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); navigateTo(ui, new NavigationTarget()); @@ -195,7 +200,8 @@ public void detachUI_uiWithDifferentWindowName_beanInScopeIsDestroyedwhenUIIsDet AtomicInteger count = new AtomicInteger(); scope.registerDestructionCallback("foo", () -> count.getAndIncrement()); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); navigateTo(ui, new NavigationTarget()); @@ -243,7 +249,8 @@ private VaadinRouteScope initScope(UI ui) { VaadinRouteScope scope = getScope(); scope.getBeanStore(); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); return scope; }