From 86b20c744f2fcd956b6416567ea60cc08418763e Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Fri, 4 Oct 2024 14:29:18 +0300 Subject: [PATCH] feat: add MenuConfiguration (#20138) * feat: add MenuConfiguration New public API for building application menu: adds `MenuConfiguration`, `MenuOptions` and `MenuOption` where `MenuConfiguration` is the main entry point to access menu data to build main menu. Fixes: #20063 * chore: renamed classes and removed MenuOptions Renamed MenuOption to MenuEntry. * chore: updated javadocs * chore: moved MenuRegistry to internal package Removed Serializable from MenuConfiguration. * chore: added javadoc and deprecated MenuData constructor * chore: use new constructor * chore: make MenuConfiguration final --- .../menu/MenuRegistry.java | 8 +- .../router/AbstractRouteNotFoundError.java | 6 - .../java/com/vaadin/flow/router/MenuData.java | 41 ++- .../AbstractNavigationStateRenderer.java | 2 +- .../internal/AbstractRouteRegistry.java | 4 +- .../router/internal/DefaultRouteResolver.java | 6 +- .../flow/server/frontend/FrontendUtils.java | 5 +- .../flow/server/menu/MenuConfiguration.java | 71 +++++ .../vaadin/flow/server/menu/MenuEntry.java | 47 +++ .../internal/JavaScriptBootstrapUITest.java | 3 +- .../flow/router/DefaultRouteResolverTest.java | 2 +- .../internal/NavigationStateRendererTest.java | 4 +- .../server/menu/MenuConfigurationTest.java | 288 ++++++++++++++++++ .../flow/server/menu/MenuRegistryTest.java | 33 +- .../testutil/ClassesSerializableTest.java | 3 +- 15 files changed, 464 insertions(+), 59 deletions(-) rename flow-server/src/main/java/com/vaadin/flow/{server => internal}/menu/MenuRegistry.java (98%) create mode 100644 flow-server/src/main/java/com/vaadin/flow/server/menu/MenuConfiguration.java create mode 100644 flow-server/src/main/java/com/vaadin/flow/server/menu/MenuEntry.java create mode 100644 flow-server/src/test/java/com/vaadin/flow/server/menu/MenuConfigurationTest.java diff --git a/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuRegistry.java b/flow-server/src/main/java/com/vaadin/flow/internal/menu/MenuRegistry.java similarity index 98% rename from flow-server/src/main/java/com/vaadin/flow/server/menu/MenuRegistry.java rename to flow-server/src/main/java/com/vaadin/flow/internal/menu/MenuRegistry.java index f703d0ff6bc..0186c50c417 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/menu/MenuRegistry.java @@ -14,7 +14,7 @@ * the License. */ -package com.vaadin.flow.server.menu; +package com.vaadin.flow.internal.menu; import java.io.IOException; import java.io.InputStream; @@ -54,6 +54,8 @@ import com.vaadin.flow.server.VaadinRequest; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.menu.AvailableViewInfo; +import com.vaadin.flow.server.menu.RouteParamType; import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED; @@ -63,6 +65,8 @@ * * Only returns views that are accessible at the moment and leaves out routes * that require path parameters. + *

+ * For internal use only. May be renamed or removed in a future release. */ public class MenuRegistry { @@ -346,7 +350,7 @@ private static void collectClientViews(String basePath, if (viewConfig.menu() == null) { // create MenuData anyway to avoid need for null checking viewConfig = copyAvailableViewInfo(viewConfig, - new MenuData(viewConfig.title(), null, false, null)); + new MenuData(viewConfig.title(), null, false, null, null)); } configurations.put(path, viewConfig); if (viewConfig.children() != null) { diff --git a/flow-server/src/main/java/com/vaadin/flow/router/AbstractRouteNotFoundError.java b/flow-server/src/main/java/com/vaadin/flow/router/AbstractRouteNotFoundError.java index abd36ab313c..76e772f98d0 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/AbstractRouteNotFoundError.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/AbstractRouteNotFoundError.java @@ -22,8 +22,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -38,12 +36,8 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Html; import com.vaadin.flow.component.Tag; -import com.vaadin.flow.di.Lookup; -import com.vaadin.flow.router.internal.ClientRoutesProvider; import com.vaadin.flow.server.HttpStatusCode; -import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.frontend.FrontendUtils; -import com.vaadin.flow.server.menu.MenuRegistry; /** * This is abstract error view for routing exceptions. diff --git a/flow-server/src/main/java/com/vaadin/flow/router/MenuData.java b/flow-server/src/main/java/com/vaadin/flow/router/MenuData.java index b59a659ed43..becfcdfac4c 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/MenuData.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/MenuData.java @@ -19,12 +19,34 @@ import java.io.Serializable; import java.util.Objects; +import com.vaadin.flow.component.Component; + /** * Data class for menu item information. *

* Only for read as data is immutable. */ -public record MenuData(String title, Double order, boolean exclude, String icon) implements Serializable { +public record MenuData(String title, Double order, boolean exclude, String icon, Class menuClass) implements Serializable { + + /** + * MenuData constructor. + * + * @param title + * title of the menu item + * @param order + * order of the menu item + * @param exclude + * whether the menu item should be excluded + * @param icon + * the icon of the menu item + * + * @deprecated Use {@link #MenuData(String, Double, boolean, String, Class)} + * instead. + */ + @Deprecated(forRemoval = true) + public MenuData(String title, Double order, boolean exclude, String icon) { + this(title, order, exclude, icon, null); + } /** * Gets the title of the menu item. @@ -65,20 +87,7 @@ public String getIcon() { @Override public String toString() { return "MenuData{" + "title='" + title + '\'' + ", order=" + order - + ", exclude=" + exclude + ", icon='" + icon + '\'' + '}'; - } - - @Override - public boolean equals(Object obj) { - return obj instanceof MenuData other - && Objects.equals(title, other.title) - && Objects.equals(order, other.order) - && Objects.equals(exclude, other.exclude) - && Objects.equals(icon, other.icon); - } - - @Override - public int hashCode() { - return Objects.hash(title, order, exclude, icon); + + ", exclude=" + exclude + ", icon='" + icon + "', menuClass='" + + menuClass + "'" + '}'; } } diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java index 2fd1dae7eb1..6c853dddd31 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java @@ -68,7 +68,7 @@ import com.vaadin.flow.server.Constants; import com.vaadin.flow.server.HttpStatusCode; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; /** * Base class for navigation handlers that target a navigation state. diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractRouteRegistry.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractRouteRegistry.java index ea650fd8dfe..56d23d5ddfe 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractRouteRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractRouteRegistry.java @@ -56,7 +56,7 @@ import com.vaadin.flow.server.auth.NavigationContext; import com.vaadin.flow.server.auth.ViewAccessChecker; import com.vaadin.flow.router.Layout; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; import com.vaadin.flow.shared.Registration; import static java.util.stream.Collectors.toList; @@ -324,7 +324,7 @@ private void populateRegisteredRoutes(ConfiguredRoutes configuration, (Objects.equals(menu.order(), Double.MIN_VALUE)) ? null : menu.order(), excludeFromMenu, - (menu.icon().isBlank() ? null : menu.icon()))) + (menu.icon().isBlank() ? null : menu.icon()), target)) .orElse(null); RouteData route = new RouteData(parentLayouts, template, parameters, diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java index 6dbfe0b4069..ce232e44348 100644 --- a/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java +++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/DefaultRouteResolver.java @@ -16,7 +16,6 @@ package com.vaadin.flow.router.internal; import java.util.Collections; -import java.util.List; import org.slf4j.LoggerFactory; @@ -25,11 +24,8 @@ import com.vaadin.flow.router.NavigationStateBuilder; import com.vaadin.flow.router.NotFoundException; import com.vaadin.flow.router.RouteResolver; -import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.server.RouteRegistry; -import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.server.menu.AvailableViewInfo; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; /** * Default implementation of the {@link RouteResolver} interface. diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java index f8935dde512..036e001d16b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java @@ -53,15 +53,12 @@ import com.vaadin.flow.internal.Pair; import com.vaadin.flow.internal.StringUtil; import com.vaadin.flow.internal.hilla.EndpointRequestUtil; -import com.vaadin.flow.router.internal.ClientRoutesProvider; import com.vaadin.flow.server.AbstractConfiguration; import com.vaadin.flow.server.Constants; -import com.vaadin.flow.server.VaadinRequest; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinServlet; -import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.frontend.scanner.ClassFinder; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; import elemental.json.JsonObject; import static com.vaadin.flow.server.Constants.COMPATIBILITY_RESOURCES_FRONTEND_DEFAULT; diff --git a/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuConfiguration.java b/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuConfiguration.java new file mode 100644 index 00000000000..b51b1f336b8 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuConfiguration.java @@ -0,0 +1,71 @@ +/* + * 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.server.menu; + +import java.io.Serializable; +import java.util.List; +import java.util.Locale; + +import com.vaadin.flow.internal.menu.MenuRegistry; + +/** + * Menu configuration helper class to retrieve available menu entries for + * application main menu. + * + * @since 24.5 + */ +public final class MenuConfiguration { + + /** + * Collect ordered list of menu entries for menu population. All client + * views are collected and any accessible server views. + * + * @return ordered list of {@link MenuEntry} instances + */ + public static List getMenuEntries() { + return MenuRegistry.collectMenuItemsList().stream() + .map(MenuConfiguration::createMenuEntry).toList(); + } + + /** + * Collect ordered list of menu entries for menu population. All client + * views are collected and any accessible server views. + * + * @param locale + * locale to use for ordering. null for default locale. + * + * @return ordered list of {@link MenuEntry} instances + */ + public static List getMenuEntries(Locale locale) { + return MenuRegistry.collectMenuItemsList(locale).stream() + .map(MenuConfiguration::createMenuEntry).toList(); + } + + private static MenuEntry createMenuEntry(AvailableViewInfo viewInfo) { + if (viewInfo.menu() == null) { + return new MenuEntry(viewInfo.route(), viewInfo.title(), null, + false, null, null); + } + return new MenuEntry(viewInfo.route(), + (viewInfo.menu().title() != null + && !viewInfo.menu().title().isBlank() + ? viewInfo.menu().title() + : viewInfo.title()), + viewInfo.menu().order(), viewInfo.menu().exclude(), + viewInfo.menu().icon(), viewInfo.menu().menuClass()); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuEntry.java b/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuEntry.java new file mode 100644 index 00000000000..769e1876efa --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/menu/MenuEntry.java @@ -0,0 +1,47 @@ +/* + * 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.server.menu; + +import java.io.Serializable; + +import com.vaadin.flow.component.Component; + +/** + * Menu entry for the main menu. + * + * @param path + * the path to navigate to + * @param title + * the title to display + * @param order + * the order in the menu or null for default order + * @param exclude + * whether to exclude the menu entry + * @param icon + * Icon to use in the menu or null for no icon. Value can go inside a + * {@code } element's {@code icon} attribute which + * accepts icon group and name like 'vaadin:file'. Or it can go to a + * {@code } element's {@code src} attribute which takes + * path to the icon. E.g. 'line-awesome/svg/lock-open-solid.svg'. + * @param menuClass + * the source class with {@link com.vaadin.flow.router.Menu} + * annotation or null if not available. Always null for + * Hilla/TypeScript client views. + */ +public record MenuEntry(String path, String title, Double order, + boolean exclude, String icon, Class menuClass) implements Serializable { +} diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java index e12db65a0fa..731fc5eb23e 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java @@ -46,8 +46,7 @@ import com.vaadin.flow.server.MockServletServiceSessionSetup; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.server.VaadinSessionState; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; import com.vaadin.tests.util.MockDeploymentConfiguration; public class JavaScriptBootstrapUITest { diff --git a/flow-server/src/test/java/com/vaadin/flow/router/DefaultRouteResolverTest.java b/flow-server/src/test/java/com/vaadin/flow/router/DefaultRouteResolverTest.java index 7e4313cb690..c1280a11280 100644 --- a/flow-server/src/test/java/com/vaadin/flow/router/DefaultRouteResolverTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/router/DefaultRouteResolverTest.java @@ -34,7 +34,7 @@ import com.vaadin.flow.router.internal.ResolveRequest; import com.vaadin.flow.server.InvalidRouteConfigurationException; import com.vaadin.flow.server.RouteRegistry; -import com.vaadin.flow.server.menu.MenuRegistry; +import com.vaadin.flow.internal.menu.MenuRegistry; public class DefaultRouteResolverTest extends RoutingTestBase { diff --git a/flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java b/flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java index 96a615920f1..2949215f047 100644 --- a/flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java @@ -27,7 +27,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import com.fasterxml.jackson.annotation.JsonProperty; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.modifier.SyntheticState; import net.bytebuddy.description.modifier.Visibility; @@ -81,8 +80,7 @@ import com.vaadin.flow.server.ServiceException; import com.vaadin.flow.server.WrappedSession; import com.vaadin.flow.server.menu.AvailableViewInfo; -import com.vaadin.flow.server.menu.MenuRegistry; -import com.vaadin.flow.server.menu.RouteParamType; +import com.vaadin.flow.internal.menu.MenuRegistry; import com.vaadin.flow.server.startup.ApplicationRouteRegistry; import com.vaadin.tests.util.AlwaysLockedVaadinSession; import com.vaadin.tests.util.MockDeploymentConfiguration; diff --git a/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuConfigurationTest.java b/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuConfigurationTest.java new file mode 100644 index 00000000000..e1f2d5fe3e8 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuConfigurationTest.java @@ -0,0 +1,288 @@ +/* + * 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.server.menu; + +import jakarta.servlet.ServletContext; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.Principal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import net.jcip.annotations.NotThreadSafe; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.vaadin.flow.di.DefaultInstantiator; +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.internal.CurrentInstance; +import com.vaadin.flow.router.RouteConfiguration; +import com.vaadin.flow.server.MockServletContext; +import com.vaadin.flow.server.MockVaadinContext; +import com.vaadin.flow.server.MockVaadinSession; +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinServletContext; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.startup.ApplicationRouteRegistry; + +import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED; +import static com.vaadin.flow.internal.menu.MenuRegistry.FILE_ROUTES_JSON_NAME; + +@NotThreadSafe +public class MenuConfigurationTest { + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + private ApplicationRouteRegistry registry; + @Mock + private MenuRegistryTest.MockService vaadinService; + private VaadinSession session; + private ServletContext servletContext; + private VaadinServletContext vaadinContext; + @Mock + private DeploymentConfiguration deploymentConfiguration; + @Mock + private VaadinRequest request; + + private AutoCloseable closeable; + + @Before + public void init() { + closeable = MockitoAnnotations.openMocks(this); + servletContext = new MockServletContext(); + vaadinContext = new MockVaadinContext(servletContext); + + registry = ApplicationRouteRegistry.getInstance(vaadinContext); + + Mockito.when(vaadinService.getRouteRegistry()).thenReturn(registry); + Mockito.when(vaadinService.getContext()).thenReturn(vaadinContext); + Mockito.when(vaadinService.getInstantiator()) + .thenReturn(new DefaultInstantiator(vaadinService)); + + Mockito.when(vaadinService.getDeploymentConfiguration()) + .thenReturn(deploymentConfiguration); + + Mockito.when(deploymentConfiguration.getFrontendFolder()) + .thenReturn(tmpDir.getRoot()); + + VaadinService.setCurrent(vaadinService); + + session = new MockVaadinSession(vaadinService) { + @Override + public VaadinService getService() { + return vaadinService; + } + }; + + VaadinSession.setCurrent(session); + + Mockito.when(request.getService()).thenReturn(vaadinService); + CurrentInstance.set(VaadinRequest.class, request); + } + + @After + public void cleanup() throws Exception { + closeable.close(); + CurrentInstance.clearAll(); + } + + @Test + public void testWithLoggedInUser_userHasRoles() throws IOException { + Mockito.when(request.getUserPrincipal()) + .thenReturn(Mockito.mock(Principal.class)); + Mockito.when(request.isUserInRole(Mockito.anyString())) + .thenReturn(true); + + File generated = tmpDir.newFolder(GENERATED); + File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME); + Files.writeString(clientFiles.toPath(), + MenuRegistryTest.testClientRouteFile); + + List menuEntries = MenuConfiguration.getMenuEntries(); + Assert.assertEquals( + "List of menu items has incorrect size. Excluded menu item like /login is not expected.", + 4, menuEntries.size()); + assertOrder(menuEntries, + new String[] { "", "/about", "/hilla", "/hilla/sub" }); + } + + @Test + public void getMenuItemsList_returnsCorrectPaths() throws IOException { + File generated = tmpDir.newFolder(GENERATED); + File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME); + Files.writeString(clientFiles.toPath(), + MenuRegistryTest.testClientRouteFile); + + RouteConfiguration routeConfiguration = RouteConfiguration + .forRegistry(registry); + Arrays.asList(MenuRegistryTest.MyRoute.class, + MenuRegistryTest.MyInfo.class, + MenuRegistryTest.MyRequiredParamRoute.class, + MenuRegistryTest.MyRequiredAndOptionalParamRoute.class, + MenuRegistryTest.MyOptionalParamRoute.class, + MenuRegistryTest.MyVarargsParamRoute.class) + .forEach(routeConfiguration::setAnnotatedRoute); + + List menuEntries = MenuConfiguration.getMenuEntries(); + Assert.assertEquals(5, menuEntries.size()); + assertOrder(menuEntries, new String[] { "", "/home", "/info", "/param", + "/param/varargs" }); + + Map mapMenuItems = menuEntries.stream() + .collect(Collectors.toMap(MenuEntry::path, item -> item)); + assertClientRoutes(mapMenuItems, false, false, true); + assertServerRoutes(mapMenuItems); + assertServerRoutesWithParameters(mapMenuItems, true); + } + + @Test + public void getMenuItemsList_assertOrder() { + RouteConfiguration routeConfiguration = RouteConfiguration + .forRegistry(registry); + Arrays.asList(MenuRegistryTest.TestRouteA.class, + MenuRegistryTest.TestRouteB.class, + MenuRegistryTest.TestRouteC.class, + MenuRegistryTest.TestRouteD.class, + MenuRegistryTest.TestRouteDA.class, + MenuRegistryTest.TestRouteDB.class) + .forEach(routeConfiguration::setAnnotatedRoute); + + List menuEntries = MenuConfiguration.getMenuEntries(); + ; + Assert.assertEquals(4, menuEntries.size()); + assertOrder(menuEntries, + new String[] { "/d", "/c", "/a", "/b", "/d/a", "/d/b" }); + } + + private void assertOrder(List menuEntries, + String[] expectedOrder) { + for (int i = 0; i < menuEntries.size(); i++) { + Assert.assertEquals(expectedOrder[i], menuEntries.get(i).path()); + } + } + + private void assertClientRoutes(Map menuOptions, + boolean authenticated, boolean hasRole, boolean excludeExpected) { + Assert.assertTrue("Client route '' missing", + menuOptions.containsKey("")); + Assert.assertEquals("Public", menuOptions.get("").title()); + + if (authenticated) { + Assert.assertTrue("Client route 'about' missing", + menuOptions.containsKey("/about")); + Assert.assertEquals("About", menuOptions.get("/about").title()); + + if (hasRole) { + Assert.assertTrue("Client route 'hilla' missing", + menuOptions.containsKey("/hilla")); + Assert.assertEquals("Hilla", menuOptions.get("/hilla").title()); + + Assert.assertTrue("Client child route 'hilla/sub' missing", + menuOptions.containsKey("/hilla/sub")); + Assert.assertEquals("Hilla Sub", + menuOptions.get("/hilla/sub").title()); + } else { + Assert.assertFalse( + "Roles do not match no hilla should be available", + menuOptions.containsKey("/hilla")); + } + } else { + Assert.assertFalse( + "Not authenticated about view should not be available", + menuOptions.containsKey("/about")); + Assert.assertFalse( + "Not authenticated hilla view should not be available", + menuOptions.containsKey("/hilla")); + } + + if (excludeExpected) { + Assert.assertFalse("Client route 'login' should be excluded", + menuOptions.containsKey("/login")); + } else { + Assert.assertTrue("Client route 'login' missing", + menuOptions.containsKey("/login")); + Assert.assertEquals("Login", menuOptions.get("/login").title()); + Assert.assertNull(menuOptions.get("/login").title()); + Assert.assertTrue("Login view should be excluded", + menuOptions.get("/login").exclude()); + } + } + + private void assertServerRoutes(Map menuItems) { + Assert.assertTrue("Server route 'home' missing", + menuItems.containsKey("/home")); + Assert.assertEquals("Home", menuItems.get("/home").title()); + Assert.assertEquals(MenuRegistryTest.MyRoute.class, + menuItems.get("/home").menuClass()); + + Assert.assertTrue("Server route 'info' missing", + menuItems.containsKey("/info")); + Assert.assertEquals("MyInfo", menuItems.get("/info").title()); + Assert.assertEquals(MenuRegistryTest.MyInfo.class, + menuItems.get("/info").menuClass()); + } + + private void assertServerRoutesWithParameters( + Map menuItems, boolean excludeExpected) { + if (excludeExpected) { + Assert.assertFalse( + "Server route '/param/:param' should be excluded", + menuItems.containsKey("/param/:param")); + Assert.assertFalse( + "Server route '/param/:param1' should be excluded", + menuItems.containsKey("/param/:param1")); + } else { + Assert.assertTrue("Server route '/param/:param' missing", + menuItems.containsKey("/param/:param")); + Assert.assertTrue( + "Server route '/param/:param' should be excluded from menu", + menuItems.get("/param/:param").exclude()); + + Assert.assertTrue("Server route '/param/:param1' missing", + menuItems.containsKey("/param/:param1")); + Assert.assertTrue( + "Server route '/param/:param1' should be excluded from menu", + menuItems.get("/param/:param1").exclude()); + } + + Assert.assertTrue( + "Server route with optional parameters '/param' missing", + menuItems.containsKey("/param")); + Assert.assertFalse( + "Server route '/param' should be included in the menu", + menuItems.get("/param").exclude()); + + Assert.assertTrue( + "Server route with optional parameters '/param/varargs' missing", + menuItems.containsKey("/param/varargs")); + Assert.assertFalse( + "Server route '/param/varargs' should be included in the menu", + menuItems.get("/param/varargs").exclude()); + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuRegistryTest.java b/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuRegistryTest.java index b6f080aedc5..36078d7b620 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuRegistryTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/menu/MenuRegistryTest.java @@ -44,6 +44,7 @@ import com.vaadin.flow.di.Instantiator; import com.vaadin.flow.function.DeploymentConfiguration; import com.vaadin.flow.internal.CurrentInstance; +import com.vaadin.flow.internal.menu.MenuRegistry; import com.vaadin.flow.router.Menu; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouteConfiguration; @@ -59,8 +60,8 @@ import com.vaadin.flow.server.startup.ApplicationRouteRegistry; import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED; -import static com.vaadin.flow.server.menu.MenuRegistry.FILE_ROUTES_JSON_NAME; -import static com.vaadin.flow.server.menu.MenuRegistry.FILE_ROUTES_JSON_PROD_PATH; +import static com.vaadin.flow.internal.menu.MenuRegistry.FILE_ROUTES_JSON_NAME; +import static com.vaadin.flow.internal.menu.MenuRegistry.FILE_ROUTES_JSON_PROD_PATH; @NotThreadSafe public class MenuRegistryTest { @@ -457,78 +458,78 @@ private void assertServerRoutesWithParameters( @Tag("div") @Route("home") @Menu(title = "Home") - private static class MyRoute extends Component { + public static class MyRoute extends Component { } @Tag("div") @Route("info") @Menu - private static class MyInfo extends Component { + public static class MyInfo extends Component { } @Tag("div") @Route("param/:param") @Menu - private static class MyRequiredParamRoute extends Component { + public static class MyRequiredParamRoute extends Component { } @Tag("div") @Route("param/:param1/:param2?") @Menu - private static class MyRequiredAndOptionalParamRoute extends Component { + public static class MyRequiredAndOptionalParamRoute extends Component { } @Tag("div") @Route("param/:param1?/:param2?(edit)") @Menu - private static class MyOptionalParamRoute extends Component { + public static class MyOptionalParamRoute extends Component { } @Tag("div") @Route("param/varargs/:param*") @Menu - private static class MyVarargsParamRoute extends Component { + public static class MyVarargsParamRoute extends Component { } @Tag("div") @Route("a") @Menu(order = 1.1) - private static class TestRouteA extends Component { + public static class TestRouteA extends Component { } @Tag("div") @Route("b") @Menu(order = 1.2) - private static class TestRouteB extends Component { + public static class TestRouteB extends Component { } @Tag("div") @Route("c") @Menu(order = 0.1) - private static class TestRouteC extends Component { + public static class TestRouteC extends Component { } @Tag("div") @Route("d") @Menu(order = 0) - private static class TestRouteD extends Component { + public static class TestRouteD extends Component { } @Tag("div") @Route("d/b") - private static class TestRouteDB extends Component { + public static class TestRouteDB extends Component { } @Tag("div") @Route("d/a") - private static class TestRouteDA extends Component { + public static class TestRouteDA extends Component { } /** * Extending class to let us mock the getRouteRegistry method for testing. */ - private static class MockService extends VaadinServletService { + public static class MockService extends VaadinServletService { @Override public RouteRegistry getRouteRegistry() { @@ -541,7 +542,7 @@ public Instantiator getInstantiator() { } } - String testClientRouteFile = """ + public static String testClientRouteFile = """ [ { "route": "", 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 c4455547c14..46d86951352 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 @@ -168,7 +168,8 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.server\\.communication\\.PushHandler(\\$.*)?", "com\\.vaadin\\.flow\\.server\\.communication\\.PushRequestHandler(\\$.*)?", "com\\.vaadin\\.flow\\.server\\.communication\\.JavaScriptBootstrapHandler(\\$.*)?", - "com\\.vaadin\\.flow\\.server\\.menu\\.MenuRegistry(\\$.*)?", + "com\\.vaadin\\.flow\\.internal\\.menu\\.MenuRegistry(\\$.*)?", + "com\\.vaadin\\.flow\\.server\\.menu\\.MenuConfiguration(\\$.*)?", "com\\.vaadin\\.flow\\.templatemodel\\.PathLookup", "com\\.vaadin\\.flow\\.server\\.startup\\.ErrorNavigationTargetInitializer", "com\\.vaadin\\.flow\\.server\\.startup\\.RouteRegistryInitializer",