diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/HotswapCompleteEvent.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/HotswapCompleteEvent.java new file mode 100644 index 00000000000..60d5089abe9 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/HotswapCompleteEvent.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.flow.hotswap; + +import java.util.Set; + +import com.vaadin.flow.server.VaadinService; + +/* + * Event fired when hotswap has been completed. + */ +public class HotswapCompleteEvent { + + private final Set> classes; + private final VaadinService vaadinService; + private final boolean redefined; + + public HotswapCompleteEvent(VaadinService vaadinService, + Set> classes, boolean redefined) { + this.classes = classes; + this.vaadinService = vaadinService; + this.redefined = redefined; + } + + /** + * Gets the classes that were updated. + * + * @return the updated classes + */ + public Set> getClasses() { + return classes; + } + + /** + * Checks if the classes were redefined (as opposed to being new classes). + * + * @return {@literal true} if the classes have been redefined by hotswap + */ + public boolean isRedefined() { + return redefined; + } + + /** + * Gets the Vaadin service. + * + * @return the vaadin service + */ + public VaadinService getService() { + return vaadinService; + } + +} diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java index 7d1a568d633..63afb16a1cc 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java @@ -265,6 +265,17 @@ private void onHotswapInternal(HashSet> classes, if (forceBrowserReload || uiTreeNeedsRefresh) { triggerClientUpdate(refreshActions, forceBrowserReload); } + + HotswapCompleteEvent event = new HotswapCompleteEvent(vaadinService, + classes, redefined); + for (VaadinHotswapper hotSwapper : hotSwappers) { + try { + hotSwapper.onHotswapComplete(event); + } catch (Exception ex) { + LOGGER.debug("Hotswap complete event handling failed for {}", + hotSwapper, ex); + } + } } /** diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/VaadinHotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/VaadinHotswapper.java index f7ad6c1a05a..d26c298e756 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/VaadinHotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/VaadinHotswapper.java @@ -98,4 +98,16 @@ default boolean onClassLoadEvent(VaadinSession vaadinSession, return false; } + /** + * Called by Vaadin hotswap entry point after all hotswap related operations + * have been completed. + * + * @param event + * an event containing information about the hotswap operation. + */ + default void onHotswapComplete(HotswapCompleteEvent event) { + // no-op by default + return; + } + } diff --git a/flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java b/flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java index 27ab8ce8245..4ae56203b0c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java @@ -28,6 +28,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import com.vaadin.flow.component.Component; @@ -157,6 +158,10 @@ public void onHotswap_noActiveSession_onlyGlobalHookCalled() { Mockito.verify(hillaHotswapper, never()).onClassLoadEvent( isA(VaadinSession.class), anySet(), anyBoolean()); + HotswapCompleteEvent hotswapCompleteEvent = new HotswapCompleteEvent( + service, classes, false); + assertOnHotswapCompleteInvoked(flowHotswapper, hotswapCompleteEvent); + assertOnHotswapCompleteInvoked(hillaHotswapper, hotswapCompleteEvent); } @Test @@ -222,6 +227,10 @@ public void onHotswap_sessionHookCalledOnlyForActiveSessions() Mockito.verify(hillaHotswapper, never()).onClassLoadEvent( isA(VaadinSession.class), anySet(), anyBoolean()); + HotswapCompleteEvent hotswapCompleteEvent = new HotswapCompleteEvent( + service, classes, true); + assertOnHotswapCompleteInvoked(flowHotswapper, hotswapCompleteEvent); + assertOnHotswapCompleteInvoked(hillaHotswapper, hotswapCompleteEvent); } @Test @@ -788,6 +797,18 @@ public Registration addServiceDestroyListener( uiInitInstalled.get()); } + private void assertOnHotswapCompleteInvoked(VaadinHotswapper hotswapper, + HotswapCompleteEvent event) { + var eventArgumentCaptor = ArgumentCaptor + .forClass(HotswapCompleteEvent.class); + Mockito.verify(hotswapper) + .onHotswapComplete(eventArgumentCaptor.capture()); + HotswapCompleteEvent capturedEvent = eventArgumentCaptor.getValue(); + Assert.assertEquals(event.getService(), capturedEvent.getService()); + Assert.assertEquals(event.getClasses(), capturedEvent.getClasses()); + Assert.assertEquals(event.isRedefined(), capturedEvent.isRedefined()); + } + @Tag("my-route") public static class MyRoute extends Component { @@ -796,6 +817,7 @@ public static class MyRoute extends Component { @Tag("my-route-with-child") public static class MyRouteWithChild extends Component implements HasComponents { + public MyRouteWithChild() { add(new MyComponent()); } @@ -803,6 +825,7 @@ public MyRouteWithChild() { @Tag("my-layout") public static class MyLayout extends Component implements RouterLayout { + } @Tag("my-layout-with-child") @@ -819,11 +842,13 @@ public void showRouterLayoutContent(HasElement content) { @Tag("my-nested-layout") public static class MyNestedLayout extends Component implements RouterLayout { + } @Tag("my-nested-layout-with-child") public static class MyNestedLayoutWithChild extends Component implements HasComponents, RouterLayout { + @Override public void showRouterLayoutContent(HasElement content) { RouterLayout.super.showRouterLayoutContent(content); diff --git a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java index 46d86951352..ec072787948 100644 --- a/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java +++ b/flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java @@ -113,6 +113,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.component\\.dnd\\.osgi\\.DndConnectorResource", "com\\.vaadin\\.flow\\.component\\.internal\\.DeadlockDetectingCompletableFuture", "com\\.vaadin\\.flow\\.function\\.VaadinApplicationInitializationBootstrap", + "com\\.vaadin\\.flow\\.hotswap\\.HotswapCompleteEvent", "com\\.vaadin\\.flow\\.hotswap\\.Hotswapper", "com\\.vaadin\\.flow\\.hotswap\\.VaadinHotswapper", "com\\.vaadin\\.flow\\.internal\\.BrowserLiveReloadAccessor",