Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add collectMenuItemsList() to MenuRegistry #19952

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand All @@ -41,6 +44,7 @@

import com.vaadin.flow.component.Component;
import com.vaadin.flow.router.BeforeEnterListener;
import com.vaadin.flow.router.MenuData;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.RouteConfiguration;
import com.vaadin.flow.router.RouteData;
Expand Down Expand Up @@ -72,7 +76,48 @@ public class MenuRegistry {
* @return routes with view information
*/
public static Map<String, AvailableViewInfo> collectMenuItems() {
return new MenuRegistry().getMenuItems(true);
Map<String, AvailableViewInfo> menuRoutes = new MenuRegistry()
.getMenuItems(true);
menuRoutes.entrySet()
.removeIf(entry -> Optional.ofNullable(entry.getValue())
.map(AvailableViewInfo::menu).map(MenuData::isExclude)
.orElse(false));
return menuRoutes;
}

/**
* Collect ordered list of views with menu annotation for automatic menu
* population. All client views are collected and any accessible server
* views.
*
* @return ordered routes with view information
*/
public static List<AvailableViewInfo> collectMenuItemsList() {
// en-US is used by default here to match with Hilla's
// createMenuItems.ts sorting algorithm.
return collectMenuItemsList(Locale.forLanguageTag("en-US"));
}

/**
* Collect ordered list of views with menu annotation for automatic 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 routes with view information
*/
public static List<AvailableViewInfo> collectMenuItemsList(Locale locale) {
return collectMenuItems().entrySet().stream().map(entry -> {
AvailableViewInfo value = entry.getValue();
return new AvailableViewInfo(value.title(), value.rolesAllowed(),
value.loginRequired(), entry.getKey(), value.lazy(),
value.register(), value.menu(), value.children(),
value.routeParameters(), value.flowLayout());
}).sorted(getMenuOrderComparator(
(locale != null ? Collator.getInstance(locale)
: Collator.getInstance())))
.toList();
}

/**
Expand Down Expand Up @@ -467,4 +512,16 @@ public static Map<String, AvailableViewInfo> getClientRoutes(
}
return clientItems;
}

private static Comparator<AvailableViewInfo> getMenuOrderComparator(
Collator collator) {
return (o1, o2) -> {
int ordersCompareTo = Optional.ofNullable(o1.menu())
.map(MenuData::getOrder).orElse(Double.MAX_VALUE)
.compareTo(Optional.ofNullable(o2.menu())
.map(MenuData::getOrder).orElse(Double.MAX_VALUE));
return ordersCompareTo != 0 ? ordersCompareTo
: collator.compare(o1.route(), o2.route());
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
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 jakarta.servlet.ServletContext;
import net.jcip.annotations.NotThreadSafe;
Expand Down Expand Up @@ -156,7 +158,7 @@ public void getMenuItemsNoFilteringContainsAllClientPaths()

Assert.assertEquals(5, menuItems.size());
// Validate as if logged in as all routes should be available
assertClientRoutes(menuItems, true, true);
assertClientRoutes(menuItems, true, true, false);
}

@Test
Expand Down Expand Up @@ -221,7 +223,7 @@ public void getMenuItemsContainBothClientAndServerPaths()
}

@Test
public void collectMenuItems_returnsCorrecPaths() throws IOException {
public void collectMenuItems_returnsCorrectPaths() throws IOException {
File generated = tmpDir.newFolder(GENERATED);
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
Files.writeString(clientFiles.toPath(), testClientRouteFile);
Expand All @@ -236,10 +238,10 @@ public void collectMenuItems_returnsCorrecPaths() throws IOException {
Map<String, AvailableViewInfo> menuItems = MenuRegistry
.collectMenuItems();

Assert.assertEquals(8, menuItems.size());
assertClientRoutes(menuItems);
Assert.assertEquals(5, menuItems.size());
assertClientRoutes(menuItems, false, false, true);
assertServerRoutes(menuItems);
assertServerRoutesWithParameters(menuItems);
assertServerRoutesWithParameters(menuItems, true);
}

@Test
Expand All @@ -257,7 +259,16 @@ public void testWithLoggedInUser_userHasRoles() throws IOException {
.getMenuItems(true);

Assert.assertEquals(5, menuItems.size());
assertClientRoutes(menuItems, true, true);
assertClientRoutes(menuItems, true, true, false);

// Verify that getMenuItemsList returns the same data
List<AvailableViewInfo> menuItemsList = MenuRegistry
.collectMenuItemsList();
Assert.assertEquals(
"List of menu items has incorrect size. Excluded menu item like /login is not expected.",
4, menuItemsList.size());
assertOrder(menuItemsList,
new String[] { "", "/about", "/hilla", "/hilla/sub" });
}

@Test
Expand All @@ -275,15 +286,62 @@ public void testWithLoggedInUser_noMatchingRoles() throws IOException {
.getMenuItems(true);

Assert.assertEquals(3, menuItems.size());
assertClientRoutes(menuItems, true, false);
assertClientRoutes(menuItems, true, false, false);
}

@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(), testClientRouteFile);

RouteConfiguration routeConfiguration = RouteConfiguration
.forRegistry(registry);
Arrays.asList(MyRoute.class, MyInfo.class, MyRequiredParamRoute.class,
MyRequiredAndOptionalParamRoute.class,
MyOptionalParamRoute.class, MyVarargsParamRoute.class)
.forEach(routeConfiguration::setAnnotatedRoute);

List<AvailableViewInfo> menuItems = MenuRegistry.collectMenuItemsList();
Assert.assertEquals(5, menuItems.size());
assertOrder(menuItems, new String[] { "", "/home", "/info", "/param",
"/param/varargs" });
// verifying that data is same as with collectMenuItems
Map<String, AvailableViewInfo> mapMenuItems = menuItems.stream()
.collect(Collectors.toMap(AvailableViewInfo::route,
item -> item));
assertClientRoutes(mapMenuItems, false, false, true);
assertServerRoutes(mapMenuItems);
assertServerRoutesWithParameters(mapMenuItems, true);
}

@Test
public void getMenuItemsList_assertOrder() {
RouteConfiguration routeConfiguration = RouteConfiguration
.forRegistry(registry);
Arrays.asList(TestRouteA.class, TestRouteB.class, TestRouteC.class,
TestRouteD.class, TestRouteDA.class, TestRouteDB.class)
.forEach(routeConfiguration::setAnnotatedRoute);

List<AvailableViewInfo> menuItems = MenuRegistry.collectMenuItemsList();
Assert.assertEquals(4, menuItems.size());
assertOrder(menuItems,
new String[] { "/d", "/c", "/a", "/b", "/d/a", "/d/b" });
}

private void assertOrder(List<AvailableViewInfo> menuItems,
String[] expectedOrder) {
for (int i = 0; i < menuItems.size(); i++) {
Assert.assertEquals(expectedOrder[i], menuItems.get(i).route());
}
}

private void assertClientRoutes(Map<String, AvailableViewInfo> menuItems) {
assertClientRoutes(menuItems, false, false);
assertClientRoutes(menuItems, false, false, false);
}

private void assertClientRoutes(Map<String, AvailableViewInfo> menuItems,
boolean authenticated, boolean hasRole) {
boolean authenticated, boolean hasRole, boolean excludeExpected) {
Assert.assertTrue("Client route '' missing", menuItems.containsKey(""));
Assert.assertEquals("Public", menuItems.get("").title());
Assert.assertNull("Public doesn't contain specific menu data",
Expand Down Expand Up @@ -328,12 +386,17 @@ private void assertClientRoutes(Map<String, AvailableViewInfo> menuItems,
menuItems.containsKey("/hilla"));
}

Assert.assertTrue("Client route 'login' missing",
menuItems.containsKey("/login"));
Assert.assertEquals("Login", menuItems.get("/login").title());
Assert.assertNull(menuItems.get("/login").menu().title());
Assert.assertTrue("Login view should be excluded",
menuItems.get("/login").menu().exclude());
if (excludeExpected) {
Assert.assertFalse("Client route 'login' should be excluded",
menuItems.containsKey("/login"));
} else {
Assert.assertTrue("Client route 'login' missing",
menuItems.containsKey("/login"));
Assert.assertEquals("Login", menuItems.get("/login").title());
Assert.assertNull(menuItems.get("/login").menu().title());
Assert.assertTrue("Login view should be excluded",
menuItems.get("/login").menu().exclude());
}
}

private void assertServerRoutes(Map<String, AvailableViewInfo> menuItems) {
Expand All @@ -350,17 +413,31 @@ private void assertServerRoutes(Map<String, AvailableViewInfo> menuItems) {

private void assertServerRoutesWithParameters(
Map<String, AvailableViewInfo> menuItems) {
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").menu().exclude());
assertServerRoutesWithParameters(menuItems, false);
}

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").menu().exclude());
private void assertServerRoutesWithParameters(
Map<String, AvailableViewInfo> 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").menu().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").menu().exclude());
}

Assert.assertTrue(
"Server route with optional parameters '/param' missing",
Expand Down Expand Up @@ -413,6 +490,41 @@ private static class MyOptionalParamRoute extends Component {
private static class MyVarargsParamRoute extends Component {
}

@Tag("div")
@Route("a")
@Menu(order = 1.1)
private static class TestRouteA extends Component {
}

@Tag("div")
@Route("b")
@Menu(order = 1.2)
private static class TestRouteB extends Component {
}

@Tag("div")
@Route("c")
@Menu(order = 0.1)
private static class TestRouteC extends Component {
}

@Tag("div")
@Route("d")
@Menu(order = 0)
private static class TestRouteD extends Component {
}

@Tag("div")
@Route("d/b")
private static class TestRouteDB extends Component {

}

@Tag("div")
@Route("d/a")
private static class TestRouteDA extends Component {
}

/**
* Extending class to let us mock the getRouteRegistry method for testing.
*/
Expand Down
Loading