Skip to content

Commit

Permalink
chore: improve route registry update logics (#19431)
Browse files Browse the repository at this point in the history
Part of #19261

Co-authored-by: Teppo Kurki <[email protected]>
  • Loading branch information
mcollovati and tepi authored Jun 11, 2024
1 parent 8b306be commit 4c0349a
Show file tree
Hide file tree
Showing 3 changed files with 563 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.util.Objects;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.internal.AnnotationReader;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLayout;

/**
Expand All @@ -37,6 +39,9 @@ public class RouteTarget implements Serializable {

private final List<Class<? extends RouterLayout>> parentLayouts;

private final boolean annotatedRoute;
private final boolean registeredAtStartup;

/**
* Create a new Route target holder with the given target registered.
*
Expand All @@ -51,6 +56,11 @@ public RouteTarget(Class<? extends Component> target,
this.parentLayouts = parents != null
? Collections.unmodifiableList(new ArrayList<>(parents))
: Collections.emptyList();
Route routeAnnotation = AnnotationReader
.getAnnotationFor(target, Route.class).orElse(null);
this.annotatedRoute = routeAnnotation != null;
this.registeredAtStartup = routeAnnotation != null
&& routeAnnotation.registerAtStartup();
}

/**
Expand All @@ -61,8 +71,7 @@ public RouteTarget(Class<? extends Component> target,
* navigation target
*/
public RouteTarget(Class<? extends Component> target) {
this.target = target;
this.parentLayouts = Collections.emptyList();
this(target, null);
}

/**
Expand Down Expand Up @@ -94,4 +103,27 @@ public List<Class<? extends RouterLayout>> getParentLayouts() {
return parentLayouts;
}

/**
* Gets if the route navigation target is a {@link Route} annotated class or
* not.
*
* @return {@literal true} if the navigation target class is annotated
* with @{@link Route} annotation, otherwise {@literal false}.
*/
boolean isAnnotatedRoute() {
return annotatedRoute;
}

/**
* Gets if this route has been registered during the initial route
* registration on application startup.
* <p>
*
* @return {@literal true} if the route was registered at application
* startup, otherwise {@literal false}.
*/
boolean isRegisteredAtStartup() {
return registeredAtStartup;
}

}
163 changes: 141 additions & 22 deletions flow-server/src/main/java/com/vaadin/flow/router/internal/RouteUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -40,6 +44,7 @@
import com.vaadin.flow.router.RoutePrefix;
import com.vaadin.flow.router.RouterLayout;
import com.vaadin.flow.server.RouteRegistry;
import com.vaadin.flow.server.SessionRouteRegistry;
import com.vaadin.flow.server.VaadinContext;

/**
Expand Down Expand Up @@ -298,6 +303,18 @@ public static String resolve(VaadinContext context, Class<?> component) {
/**
* Updates route registry as necessary when classes have been added /
* modified / deleted.
* <p>
* </p>
* Registry Update rules:
* <ul>
* <li>a route is preserved if the class does not have a {@link Route}
* annotation and did not have it at registration time</li>
* <li>a route is preserved if the class is annotated with {@link Route} and
* {@code registerAtStartup=false} and the the flag has not changed</li>
* <li>new classes are not automatically added to session registries</li>
* <li>existing routes in session registries are not removed in case of
* class modification</li>
* </ul>
*
* @param registry
* route registry
Expand All @@ -311,35 +328,137 @@ public static String resolve(VaadinContext context, Class<?> component) {
public static void updateRouteRegistry(RouteRegistry registry,
Set<Class<?>> addedClasses, Set<Class<?>> modifiedClasses,
Set<Class<?>> deletedClasses) {
RouteConfiguration routeConf = RouteConfiguration.forRegistry(registry);

if ((addedClasses == null || addedClasses.isEmpty())
&& (modifiedClasses == null || modifiedClasses.isEmpty())
&& (deletedClasses == null || deletedClasses.isEmpty())) {
// No changes to apply
return;
}
Logger logger = LoggerFactory.getLogger(RouteUtil.class);

// safe copy to prevent concurrent modification or operation failures on
// immutable sets
modifiedClasses = modifiedClasses != null
? new HashSet<>(modifiedClasses)
: new HashSet<>();
addedClasses = addedClasses != null ? new HashSet<>(addedClasses)
: new HashSet<>();
deletedClasses = deletedClasses != null ? new HashSet<>(deletedClasses)
: new HashSet<>();

// the same class may be present on more than one input sets depending
// on how IDE and agent collect file change events.
// A modified call takes over added and deleted.
if (!modifiedClasses.isEmpty()) {
addedClasses.removeIf(modifiedClasses::contains);
deletedClasses.removeIf(modifiedClasses::contains);
}

RouteConfiguration routeConf = RouteConfiguration.forRegistry(registry);

// collect classes for that are no more Flow components and should be
// removed from the registry
// NOTE: not all agents/JVMs support reload on class hierarchy change
Set<Class<?>> nonFlowComponentsToRemove = new HashSet<>();
deletedClasses.stream()
.filter(clazz -> !Component.class.isAssignableFrom(clazz))
.forEach(nonFlowComponentsToRemove::add);
modifiedClasses.stream()
.filter(clazz -> !Component.class.isAssignableFrom(clazz))
.forEach(nonFlowComponentsToRemove::add);

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

if (registry instanceof AbstractRouteRegistry abstractRouteRegistry) {
Map<String, RouteTarget> routesMap = abstractRouteRegistry
.getConfiguration().getRoutesMap();
Map<? extends Class<? extends Component>, RouteTarget> routeTargets = registry
.getRegisteredRoutes().stream()
.map(routeData -> routesMap.get(routeData.getTemplate()))
.collect(Collectors.toMap(RouteTarget::getTarget,
Function.identity()));
modifiedClassesRouteRemovalFilter = modifiedClassesRouteRemovalFilter
.and(clazz -> {
RouteTarget routeTarget = routeTargets.get(clazz);
if (routeTarget == null) {
return true;
}
boolean wasAnnotatedRoute = routeTarget
.isAnnotatedRoute();
boolean wasRegisteredAtStartup = routeTarget
.isRegisteredAtStartup();
boolean isAnnotatedRoute = clazz
.isAnnotationPresent(Route.class);
boolean isRegisteredAtStartup = isAnnotatedRoute
&& clazz.getAnnotation(Route.class)
.registerAtStartup();
if (!isAnnotatedRoute && !wasAnnotatedRoute) {
// route was previously registered manually, do not
// remove it
return false;
}
if (isAnnotatedRoute && wasAnnotatedRoute
&& !isRegisteredAtStartup
&& !wasRegisteredAtStartup) {
// a lazy annotated route has changed, but it was
// previously registered manually, do not remove it
return false;
}
return !isAnnotatedRoute || !isRegisteredAtStartup;
});
}
Stream<Class<? extends Component>> toRemove = Stream
.concat(filterComponentClasses(deletedClasses),
filterComponentClasses(modifiedClasses)
.filter(modifiedClassesRouteRemovalFilter))
.distinct();

Stream<Class<? extends Component>> toAdd;
if (isSessionRegistry) {
// routes on session registry are initialized programmatically so
// new classes should never be added automatically
toAdd = Stream.empty();
} else {
// New classes should be added to the registry only if they have a
// @Route annotation with registerAtStartup=true
toAdd = Stream
.concat(filterComponentClasses(addedClasses),
filterComponentClasses(modifiedClasses))
.filter(clazz -> clazz.isAnnotationPresent(Route.class)
&& clazz.getAnnotation(Route.class)
.registerAtStartup())
.distinct();
}

registry.update(() -> {
// remove potential routes for classes that are not Flow
// components anymore
nonFlowComponentsToRemove
.forEach(clazz -> routeConf.removeRoute((Class) clazz));
// remove deleted classes and classes that lost the annotation from
// registry
Stream.concat(deletedClasses.stream(),
modifiedClasses.stream().filter(
clazz -> !clazz.isAnnotationPresent(Route.class)))
.filter(Component.class::isAssignableFrom)
.forEach(clazz -> {
Class<? extends Component> componentClass = (Class<? extends Component>) clazz;
logger.debug("Removing route to {}", componentClass);
routeConf.removeRoute(componentClass);
});
toRemove.forEach(componentClass -> {
logger.debug("Removing route to {}", componentClass);
routeConf.removeRoute(componentClass);
});
// add new routes to registry
Stream.concat(addedClasses.stream(), modifiedClasses.stream())
.distinct().filter(Component.class::isAssignableFrom)
.filter(clazz -> clazz.isAnnotationPresent(Route.class))
.forEach(clazz -> {
Class<? extends Component> componentClass = (Class<? extends Component>) clazz;
logger.debug(
"Updating route {} to {}", componentClass
.getAnnotation(Route.class).value(),
clazz);
routeConf.removeRoute(componentClass);
routeConf.setAnnotatedRoute(componentClass);
});
toAdd.forEach(componentClass -> {
logger.debug("Updating route {} to {}",
componentClass.getAnnotation(Route.class).value(),
componentClass);
routeConf.removeRoute(componentClass);
routeConf.setAnnotatedRoute(componentClass);
});
});
}

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

}
Loading

0 comments on commit 4c0349a

Please sign in to comment.