Skip to content

Commit 2db760e

Browse files
authored
feat: allow forcing Hotswapper to reload the page (#21157)
By default, on class change, Hotswapper determines the best strategy to refresh the browser page. However, in some situations, a full page reload may be a better option. This change allows forcing Hotswapper to always trigger a full page reload if the `vaadin.hotswap.forcePageReload` system property is set to `true`. In addition, the `Hotswapper.forcePageReload()` method can be called to change the behavior at runtime instead of setting the system property. Fixes #20844
1 parent 15b4251 commit 2db760e

File tree

2 files changed

+170
-5
lines changed

2 files changed

+170
-5
lines changed

flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.vaadin.flow.hotswap;
1818

19+
import java.io.Serializable;
1920
import java.net.URI;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
@@ -29,6 +30,7 @@
2930
import java.util.ResourceBundle;
3031
import java.util.Set;
3132
import java.util.concurrent.ConcurrentHashMap;
33+
import java.util.concurrent.atomic.AtomicBoolean;
3234
import java.util.function.Consumer;
3335
import java.util.stream.Collectors;
3436

@@ -84,6 +86,13 @@
8486
* {@link VaadinHotswapper} interface.
8587
* <p>
8688
* </p>
89+
* By default, Hotswapper determines the best browser page refresh strategy, but
90+
* a full page reload can be forced by setting the
91+
* {@code vaadin.hotswap.forcePageReload} system property. Hotswap tools can
92+
* alter the behavior at runtime by calling
93+
* {@link #forcePageReload(VaadinService, boolean)}
94+
* <p>
95+
* </p>
8796
* For internal use only. May be renamed or removed in a future release.
8897
*
8998
* @author Vaadin Ltd
@@ -92,6 +101,14 @@
92101
*/
93102
public class Hotswapper implements ServiceDestroyListener, SessionInitListener,
94103
SessionDestroyListener, UIInitListener {
104+
105+
/**
106+
* Configuration name for the system parameter that determines whether
107+
* Hotswapper should always trigger a full page reload instead of computing
108+
* an appropriate UI refresh strategy.
109+
*/
110+
public static final String FORCE_RELOAD_PROPERTY = "vaadin.hotswap.forcePageReload";
111+
95112
private static final Logger LOGGER = LoggerFactory
96113
.getLogger(Hotswapper.class);
97114
private final Set<VaadinSession> sessions = ConcurrentHashMap.newKeySet();
@@ -259,9 +276,18 @@ private void onHotswapInternal(HashSet<Class<?>> classes,
259276
vaadinSession.getLockInstance().unlock();
260277
}
261278
}
262-
EnumMap<UIRefreshStrategy, List<UI>> refreshActions = computeRefreshStrategies(
263-
classes, redefined);
264-
boolean uiTreeNeedsRefresh = !refreshActions.isEmpty();
279+
forceBrowserReload = forceBrowserReload
280+
|| getForceReloadHolder(vaadinService).shouldReloadPage();
281+
282+
boolean uiTreeNeedsRefresh = false;
283+
EnumMap<UIRefreshStrategy, List<UI>> refreshActions = null;
284+
285+
// When a full page reload is requested it does not make sense to
286+
// compute refresh strategy
287+
if (!forceBrowserReload) {
288+
refreshActions = computeRefreshStrategies(classes, redefined);
289+
uiTreeNeedsRefresh = !refreshActions.isEmpty();
290+
}
265291
if (forceBrowserReload || uiTreeNeedsRefresh) {
266292
triggerClientUpdate(refreshActions, forceBrowserReload);
267293
}
@@ -447,8 +473,8 @@ private void triggerClientUpdate(
447473
EnumMap<UIRefreshStrategy, List<UI>> uisToRefresh,
448474
boolean forceReload) {
449475

450-
boolean refreshRequested = uisToRefresh
451-
.containsKey(UIRefreshStrategy.REFRESH);
476+
boolean refreshRequested = !forceReload
477+
&& uisToRefresh.containsKey(UIRefreshStrategy.REFRESH);
452478

453479
// If some UI has push not enabled, BrowserLiveReload should be used to
454480
// trigger a client update. However, BrowserLiveReload broadcasts the
@@ -548,4 +574,57 @@ public static Optional<Hotswapper> register(VaadinService vaadinService) {
548574
return Optional.empty();
549575
}
550576

577+
/**
578+
* Instructs the {@link Hotswapper} if a full page reload should always be
579+
* triggered instead of detecting the best UI refresh strategy.
580+
*
581+
* @param vaadinService
582+
* the {@link VaadinService} instance.
583+
* @param forceReload
584+
* {@literal true} to always force a page reload,
585+
* {@literal false} to let the {@link Hotswapper} decide the
586+
* refresh strategy.
587+
*/
588+
public static void forcePageReload(VaadinService vaadinService,
589+
boolean forceReload) {
590+
Objects.requireNonNull(vaadinService, "VaadinService cannot be null");
591+
getForceReloadHolder(vaadinService).activate(forceReload);
592+
}
593+
594+
/**
595+
* Gets whether a forced full page reload is triggered on class changes.
596+
*
597+
* @param vaadinService
598+
* the {@link VaadinService} instance.
599+
* @return {@literal true} if full page reload if forced, otherwise
600+
* {@literal false}.
601+
*/
602+
public static boolean isForcedPageReload(VaadinService vaadinService) {
603+
return getForceReloadHolder(vaadinService).isActive();
604+
}
605+
606+
private static ForcePageReloadHolder getForceReloadHolder(
607+
VaadinService vaadinService) {
608+
return vaadinService.getContext().getAttribute(
609+
ForcePageReloadHolder.class, ForcePageReloadHolder::new);
610+
}
611+
612+
private static class ForcePageReloadHolder implements Serializable {
613+
614+
private final AtomicBoolean forceReload = new AtomicBoolean(false);
615+
616+
void activate(boolean active) {
617+
forceReload.set(active);
618+
}
619+
620+
boolean isActive() {
621+
return forceReload.get();
622+
}
623+
624+
boolean shouldReloadPage() {
625+
return forceReload.get()
626+
|| Boolean.getBoolean(FORCE_RELOAD_PROPERTY);
627+
}
628+
}
629+
551630
}

flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,92 @@ public void onHotswap_mixedPushState_classInUITreeChanged_liveReloadTriggered()
943943
Mockito.verify(liveReload).refresh(anyBoolean());
944944
}
945945

946+
@Test
947+
public void onHotswap_pushDisabled_forcePageReload_fullReloadTriggered()
948+
throws ServiceException {
949+
VaadinSession session = createMockVaadinSession();
950+
hotswapper.sessionInit(new SessionInitEvent(service, session, null));
951+
952+
RefreshTestingUI ui = initUIAndNavigateTo(session, MyRoute.class);
953+
954+
Hotswapper.forcePageReload(service, true);
955+
hotswapper.onHotswap(new String[] { MyRoute.class.getName() }, true);
956+
957+
ui.assertNotRefreshed();
958+
Mockito.verify(liveReload, never()).refresh(anyBoolean());
959+
Mockito.verify(liveReload).reload();
960+
}
961+
962+
@Test
963+
public void onHotswap_pushDisabled_forcePageReloadWithSystemProperty_fullReloadTriggered()
964+
throws ServiceException {
965+
VaadinSession session = createMockVaadinSession();
966+
hotswapper.sessionInit(new SessionInitEvent(service, session, null));
967+
968+
RefreshTestingUI ui = initUIAndNavigateTo(session, MyRoute.class);
969+
970+
String reload = System.getProperty(Hotswapper.FORCE_RELOAD_PROPERTY);
971+
System.setProperty(Hotswapper.FORCE_RELOAD_PROPERTY, "true");
972+
try {
973+
hotswapper.onHotswap(new String[] { MyRoute.class.getName() },
974+
true);
975+
} finally {
976+
if (reload != null) {
977+
System.setProperty(Hotswapper.FORCE_RELOAD_PROPERTY, reload);
978+
} else {
979+
System.clearProperty(Hotswapper.FORCE_RELOAD_PROPERTY);
980+
}
981+
}
982+
983+
ui.assertNotRefreshed();
984+
Mockito.verify(liveReload, never()).refresh(anyBoolean());
985+
Mockito.verify(liveReload).reload();
986+
}
987+
988+
@Test
989+
public void onHotswap_pushEnabled_forcePageReload_fullReloadTriggered()
990+
throws ServiceException {
991+
VaadinSession session = createMockVaadinSession();
992+
hotswapper.sessionInit(new SessionInitEvent(service, session, null));
993+
994+
RefreshTestingUI ui = initUIAndNavigateTo(session, MyRoute.class);
995+
ui.enablePush();
996+
997+
Hotswapper.forcePageReload(service, true);
998+
hotswapper.onHotswap(new String[] { MyRoute.class.getName() }, true);
999+
1000+
ui.assertNotRefreshed();
1001+
Mockito.verify(liveReload, never()).refresh(anyBoolean());
1002+
Mockito.verify(liveReload).reload();
1003+
}
1004+
1005+
@Test
1006+
public void onHotswap_pushEnabled_forcePageReloadWithSystemProperty_fullReloadTriggered()
1007+
throws ServiceException {
1008+
VaadinSession session = createMockVaadinSession();
1009+
hotswapper.sessionInit(new SessionInitEvent(service, session, null));
1010+
1011+
RefreshTestingUI ui = initUIAndNavigateTo(session, MyRoute.class);
1012+
ui.enablePush();
1013+
1014+
String reload = System.getProperty(Hotswapper.FORCE_RELOAD_PROPERTY);
1015+
System.setProperty(Hotswapper.FORCE_RELOAD_PROPERTY, "true");
1016+
try {
1017+
hotswapper.onHotswap(new String[] { MyRoute.class.getName() },
1018+
true);
1019+
} finally {
1020+
if (reload != null) {
1021+
System.setProperty(Hotswapper.FORCE_RELOAD_PROPERTY, reload);
1022+
} else {
1023+
System.clearProperty(Hotswapper.FORCE_RELOAD_PROPERTY);
1024+
}
1025+
}
1026+
1027+
ui.assertNotRefreshed();
1028+
Mockito.verify(liveReload, never()).refresh(anyBoolean());
1029+
Mockito.verify(liveReload).reload();
1030+
}
1031+
9461032
@Test
9471033
public void register_developmentMode_trackingListenerInstalled() {
9481034
AtomicBoolean sessionInitInstalled = new AtomicBoolean();

0 commit comments

Comments
 (0)