Skip to content

Commit

Permalink
fix: reload route configuration upon layout changes (#20139)
Browse files Browse the repository at this point in the history
* fix: reload route configuration upon layout changes

When a [At]Layout annotated class is modified, the changes are not
propagated to the route registry after hotswap happens.
This change updates Route registry layouts configuration and re-registers
routes potentially impacted by the change to apply the new settings.
It also checks the route target chain for active UIs in order to
trigger a page refresh if the layout changes should be applied.

Fixes #20111

* clean up and remove useless duplicated test

* revert change

* handle layout changes for Hilla views

* fix assertion parameters oreder

* remove commented code
  • Loading branch information
mcollovati authored and vaadin-bot committed Oct 7, 2024
1 parent 8d0e36c commit ea334c5
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 45 deletions.
15 changes: 15 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.BrowserLiveReload;
import com.vaadin.flow.internal.BrowserLiveReloadAccessor;
import com.vaadin.flow.router.internal.RouteTarget;
import com.vaadin.flow.server.RouteRegistry;
import com.vaadin.flow.server.ServiceDestroyEvent;
import com.vaadin.flow.server.ServiceDestroyListener;
import com.vaadin.flow.server.ServiceException;
Expand Down Expand Up @@ -309,6 +311,19 @@ private UIRefreshStrategy computeRefreshStrategy(UI ui,
refreshStrategy = computeRefreshStrategyForUITree(ui,
changedClasses, targetsChain, route);
}
// A different layout might have been applied after hotswap
if (refreshStrategy == UIRefreshStrategy.SKIP) {
RouteRegistry registry = ui.getInternals().getRouter()
.getRegistry();
RouteTarget routeTarget = registry
.getNavigationRouteTarget(
ui.getActiveViewLocation().getPath())
.getRouteTarget();
if (routeTarget != null && routeTarget.getParentLayouts().stream()
.anyMatch(changedClasses::contains)) {
refreshStrategy = UIRefreshStrategy.PUSH_REFRESH_CHAIN;
}
}

// If push is not enabled we can only request a full page refresh
if (refreshStrategy != UIRefreshStrategy.SKIP && !pushEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;
Expand All @@ -35,6 +36,7 @@
import com.vaadin.flow.internal.ReflectTools;
import com.vaadin.flow.router.BeforeEnterListener;
import com.vaadin.flow.router.HasErrorParameter;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.MenuData;
import com.vaadin.flow.router.NotFoundException;
Expand All @@ -55,7 +57,6 @@
import com.vaadin.flow.server.auth.NavigationAccessControl;
import com.vaadin.flow.server.auth.NavigationContext;
import com.vaadin.flow.server.auth.ViewAccessChecker;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.internal.menu.MenuRegistry;
import com.vaadin.flow.shared.Registration;

Expand Down Expand Up @@ -611,6 +612,23 @@ public void setLayout(Class<? extends RouterLayout> layout) {
}
}

void updateLayout(Class<? extends RouterLayout> layout) {
if (layout == null) {
return;
}
synchronized (layouts) {
layouts.entrySet()
.removeIf(entry -> layout.equals(entry.getValue()));
if (layout.isAnnotationPresent(Layout.class)) {
layouts.put(layout.getAnnotation(Layout.class).value(), layout);
}
}
}

Collection<Class<?>> getLayouts() {
return Set.copyOf(layouts.values());
}

@Override
public Class<? extends RouterLayout> getLayout(String path) {
Optional<String> first = layouts.keySet().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@

package com.vaadin.flow.router.internal;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.hotswap.VaadinHotswapper;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.SessionRouteRegistry;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.Options;
import com.vaadin.flow.server.frontend.TaskGenerateReactFiles;
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;

/**
Expand Down Expand Up @@ -65,8 +71,34 @@ public boolean onClassLoadEvent(VaadinService vaadinService,
removedClasses)) {
ApplicationRouteRegistry appRegistry = ApplicationRouteRegistry
.getInstance(vaadinService.getContext());
// Collect layouts before and after changes to trigger layouts.json
// regeneration if something changed
Set<Class<?>> layouts = new HashSet<>();
appRegistry.getRegisteredRoutes().stream()
.flatMap(rd -> rd.getParentLayouts().stream())
.collect(Collectors.toCollection(() -> layouts));

RouteUtil.updateRouteRegistry(appRegistry, addedClasses,
modifiedClasses, removedClasses);

appRegistry.getRegisteredRoutes().stream()
.flatMap(rd -> rd.getParentLayouts().stream())
.collect(Collectors.toCollection(() -> layouts));

DeploymentConfiguration configuration = vaadinService
.getDeploymentConfiguration();
if (configuration.isReactEnabled()
&& Stream.of(addedClasses, modifiedClasses, removedClasses)
.flatMap(Set::stream).anyMatch(layouts::contains)) {

Options options = new Options(
vaadinService.getContext().getAttribute(Lookup.class),
null, configuration.getProjectFolder())
.withFrontendDirectory(
configuration.getFrontendFolder());
TaskGenerateReactFiles.writeLayouts(options,
((AbstractRouteRegistry) appRegistry).getLayouts());
}
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.vaadin.flow.router.ParentLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteAlias;
import com.vaadin.flow.router.RouteBaseData;
import com.vaadin.flow.router.RouteConfiguration;
import com.vaadin.flow.router.RoutePathProvider;
import com.vaadin.flow.router.RoutePrefix;
Expand Down Expand Up @@ -411,18 +412,39 @@ public static void updateRouteRegistry(RouteRegistry registry,
modifiedClasses.stream()
.filter(clazz -> !Component.class.isAssignableFrom(clazz))
.forEach(nonFlowComponentsToRemove::add);
Set<Class<? extends RouterLayout>> layouts = new HashSet<>();

boolean isSessionRegistry = registry instanceof SessionRouteRegistry;
Predicate<Class<? extends Component>> modifiedClassesRouteRemovalFilter = clazz -> !isSessionRegistry;

if (registry instanceof AbstractRouteRegistry abstractRouteRegistry) {

// update layouts
filterLayoutClasses(deletedClasses).forEach(layouts::add);
filterLayoutClasses(modifiedClasses).forEach(layouts::add);
filterLayoutClasses(addedClasses).forEach(layouts::add);
layouts.forEach(abstractRouteRegistry::updateLayout);
if (!layouts.isEmpty()) {
// Gather routes that don't have a layout or reference a layout
// that has been changed.
// Mark these routes as modified so they can be re-registered
// with the correct layouts applied.
registry.getRegisteredRoutes().stream()
.filter(rd -> rd.getParentLayouts().isEmpty()
|| rd.getParentLayouts().stream()
.anyMatch(layouts::contains))
.map(RouteBaseData::getNavigationTarget)
.forEach(modifiedClasses::add);
}

Map<String, RouteTarget> routesMap = abstractRouteRegistry
.getConfiguration().getRoutesMap();
Map<? extends Class<? extends Component>, RouteTarget> routeTargets = registry
.getRegisteredRoutes().stream()
.map(routeData -> routesMap.get(routeData.getTemplate()))
.filter(Objects::nonNull).collect(Collectors.toMap(
RouteTarget::getTarget, Function.identity()));

modifiedClassesRouteRemovalFilter = modifiedClassesRouteRemovalFilter
.and(clazz -> {
RouteTarget routeTarget = routeTargets.get(clazz);
Expand Down Expand Up @@ -498,6 +520,14 @@ public static void updateRouteRegistry(RouteRegistry registry,
});
}

@SuppressWarnings("unchecked")
private static Stream<Class<? extends RouterLayout>> filterLayoutClasses(
Set<Class<?>> classes) {
return filterComponentClasses(classes)
.filter(RouterLayout.class::isAssignableFrom)
.map(clazz -> (Class<? extends RouterLayout>) clazz);
}

@SuppressWarnings("unchecked")
private static Stream<Class<? extends Component>> filterComponentClasses(
Set<Class<?>> classes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Set;
import java.util.Collection;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
Expand All @@ -37,6 +38,7 @@
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;

import static com.vaadin.flow.server.frontend.FileIOUtils.compareIgnoringIndentationEOLAndWhiteSpace;
import static java.nio.charset.StandardCharsets.UTF_8;

Expand Down Expand Up @@ -164,8 +166,8 @@ private void doExecute() throws ExecutionFailedException {
writeFile(flowTsx, getFileContent(FLOW_TSX));
writeFile(vaadinReactTsx,
getVaadinReactTsContent(routesTsx.exists()));
writeFile(new File(frontendGeneratedFolder, LAYOUTS_JSON),
layoutsContent());
writeLayoutsJson(
options.getClassFinder().getAnnotatedClasses(Layout.class));
if (fileAvailable(REACT_ADAPTER_TEMPLATE)) {
String reactAdapterContent = getFileContent(
REACT_ADAPTER_TEMPLATE);
Expand Down Expand Up @@ -201,15 +203,48 @@ && serverRoutesAvailable()) {
}
}

private String layoutsContent() {
/**
* Writes the `layout.json` file in the frontend generated folder.
* <p>
* </p>
*
* @param options
* the task options
* @param layoutsClasses
* {@link Layout} annotated classes.
*/
public static void writeLayouts(Options options,
Collection<Class<?>> layoutsClasses) {
TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
try {
task.writeLayoutsJson(layoutsClasses);
} catch (ExecutionFailedException e) {
if (e.getCause() instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e.getCause() instanceof IOException ioEx) {
throw new UncheckedIOException(ioEx);
}
throw new RuntimeException(e.getCause());
}
}

private void writeLayoutsJson(Collection<Class<?>> layoutClasses)
throws ExecutionFailedException {
writeFile(new File(options.getFrontendGeneratedFolder(), LAYOUTS_JSON),
layoutsContent(layoutClasses));

}

private String layoutsContent(Collection<Class<?>> layoutClasses) {
JsonArray availableLayouts = Json.createArray();
Set<Class<?>> layoutClasses = options.getClassFinder()
.getAnnotatedClasses(Layout.class);
for (Class<?> layout : layoutClasses) {
JsonObject layoutObject = Json.createObject();
layoutObject.put("path",
layout.getAnnotation(Layout.class).value());
availableLayouts.set(availableLayouts.length(), layoutObject);
if (layout.isAnnotationPresent(Layout.class)) {
JsonObject layoutObject = Json.createObject();
layoutObject.put("path",
layout.getAnnotation(Layout.class).value());
availableLayouts.set(availableLayouts.length(), layoutObject);
}
}
return availableLayouts.toJson();
}
Expand Down
Loading

0 comments on commit ea334c5

Please sign in to comment.