Skip to content

Developer Guide

This section covers the data model, configuration reference, required integration points, and extension points available to developers integrating Dynamic Menu into an application.

Data Model

Dynamic Menu persists three core entities and several supporting tables. A menu item defines a navigation entry in the hierarchy. A security constraint controls which roles can see a given item. A URL parameter provides key-value substitutions for parameterised internal routes. Security constraints store their role lists in three dedicated collection tables.

Two columns of dm_menu_item decide how an item is rendered rather than where it points. icon_size holds an IconSize constant (SMALL, MEDIUM, LARGE) and is null when the item follows the application-wide default. only_render_if_allowed makes the item's visibility depend on the application's own access rules instead of its security constraint; it is nullable so that items persisted before the flag existed keep the previous behaviour.

---
config:
  layout: elk
---
erDiagram
    dm_menu_item {
        integer id                     PK
        varchar name
        varchar label
        varchar tooltip
        varchar icon
        varchar icon_type
        varchar icon_size
        integer parent_id              FK
        integer item_index
        integer permissions_id         FK
        boolean is_separator
        varchar internal_url
        varchar external_url
        boolean icon_spaced
        boolean expanded_by_default
        boolean internationalized
        boolean only_render_if_allowed
    }
    dm_sc {
        integer id PK
    }
    dm_sc_all_granted {
        integer security_constraint_entity_id FK
        varchar element
    }
    dm_sc_any_granted {
        integer security_constraint_entity_id FK
        varchar element
    }
    dm_sc_not_granted {
        integer security_constraint_entity_id FK
        varchar element
    }
    dm_url_parameter {
        integer id              PK
        integer item_id         FK
        varchar parameter_key
        varchar parameter_value
    }

    dm_menu_item  }o--o|  dm_menu_item    : "parent / children"
    dm_menu_item  ||--o|  dm_sc           : "has permissions"
    dm_sc         ||--o{  dm_sc_all_granted : "all roles"
    dm_sc         ||--o{  dm_sc_any_granted : "any role"
    dm_sc         ||--o{  dm_sc_not_granted : "not roles"
    dm_menu_item  ||--o{  dm_url_parameter  : "has parameters"
Hold "Alt" / "Option" to enable pan & zoom

Database Schema

Dynamic Menu manages seven tables. The schema below represents the DDL generated by Hibernate for a standard relational database.

CREATE SEQUENCE dm_menu_item_seq
    START WITH 1 INCREMENT BY 50;

CREATE SEQUENCE dm_sc_seq
    START WITH 1 INCREMENT BY 50;

CREATE SEQUENCE dm_url_parameter_seq
    START WITH 1 INCREMENT BY 50;

CREATE TABLE dm_sc (
    id INTEGER NOT NULL,
    CONSTRAINT pk_dm_sc PRIMARY KEY (id)
);

CREATE TABLE dm_menu_item (
    id                     INTEGER NOT NULL,
    name                   VARCHAR(255),
    label                  VARCHAR(255),
    tooltip                VARCHAR(255),
    icon                   VARCHAR(255),
    icon_type              VARCHAR(255),
    icon_size              VARCHAR(255),
    parent_id              INTEGER,
    item_index             INTEGER,
    permissions_id         INTEGER,
    is_separator           BOOLEAN,
    internal_url           VARCHAR(255),
    external_url           VARCHAR(255),
    icon_spaced            BOOLEAN,
    expanded_by_default    BOOLEAN,
    internationalized      BOOLEAN,
    only_render_if_allowed BOOLEAN,
    CONSTRAINT pk_dm_menu_item             PRIMARY KEY (id),
    CONSTRAINT fk_dm_menu_item_parent      FOREIGN KEY (parent_id)      REFERENCES dm_menu_item (id),
    CONSTRAINT fk_dm_menu_item_permissions FOREIGN KEY (permissions_id) REFERENCES dm_sc (id)
);

CREATE TABLE dm_sc_all_granted (
    security_constraint_entity_id INTEGER      NOT NULL,
    element                       VARCHAR(255),
    CONSTRAINT fk_dm_sc_all_granted FOREIGN KEY (security_constraint_entity_id) REFERENCES dm_sc (id)
);

CREATE TABLE dm_sc_any_granted (
    security_constraint_entity_id INTEGER      NOT NULL,
    element                       VARCHAR(255),
    CONSTRAINT fk_dm_sc_any_granted FOREIGN KEY (security_constraint_entity_id) REFERENCES dm_sc (id)
);

CREATE TABLE dm_sc_not_granted (
    security_constraint_entity_id INTEGER      NOT NULL,
    element                       VARCHAR(255),
    CONSTRAINT fk_dm_sc_not_granted FOREIGN KEY (security_constraint_entity_id) REFERENCES dm_sc (id)
);

CREATE TABLE dm_url_parameter (
    id              INTEGER NOT NULL,
    item_id         INTEGER NOT NULL,
    parameter_key   VARCHAR(255),
    parameter_value VARCHAR(255),
    CONSTRAINT pk_dm_url_parameter      PRIMARY KEY (id),
    CONSTRAINT fk_dm_url_parameter_item FOREIGN KEY (item_id) REFERENCES dm_menu_item (id)
);

dm_sc is created before dm_menu_item because dm_menu_item holds the foreign key to it (permissions_id). The dm_menu_item.parent_id column is a self-referential foreign key that is nullable at the top level. The item_index column records the position of an item among its siblings and is managed automatically by JPA lifecycle callbacks.

Module Overview

Dynamic Menu is structured as six Maven modules following the AppJars layered architecture:

Module Artifact ID Description
Model appjars-dynamic-menu-model DTOs, enums, JSON serializers/deserializers, the DynamicMenuAuthorityUtils interface, auto-configuration
Business API appjars-dynamic-menu-business MenuItemService interface
Business Impl appjars-dynamic-menu-business-impl Service implementation with hierarchy, drag-drop, and import logic, plus the default DynamicMenuAuthorityUtils
Data API appjars-dynamic-menu-data MenuItemDao interface
Data Impl appjars-dynamic-menu-data-impl JPA entities and DAO implementation
Flow UI appjars-dynamic-menu-flow Vaadin views, DynamicMenuItemProvider, MenuItemFactory, icon families, route configuration

A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.

Spring Auto-Configuration

Dynamic Menu registers itself through Spring Boot's auto-configuration mechanism. The entry point is DynamicMenuAutoConfiguration, declared in:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

This class scans the com.appjars.dynamicmenu package for all Spring components and JPA entities. Two beans are registered by that scan and are meant to be replaced by the application:

  • DynamicMenuDefaultAuthorityUtilsImpl, a DynamicMenuAuthorityUtils returning a placeholder set of two roles (ADMIN, USER) with ANONYMOUS as the unauthenticated authority. Sufficient for initial development, but it must be replaced before going to production (see below).
  • DefaultIconFamilyProvider, an IconFamilyProvider exposing only the Vaadin icon set. See Offering Additional Icon Families.

Configuration Properties

Property Default Description
com.appjars.dynamicmenu.url.views.menuitemsview dm/list URL path of the menu item management view
appjars.dynamicmenu.icon.size MEDIUM Application-wide default size for menu item icons, which each item can override. Accepted values, case-insensitive: SMALL, MEDIUM, LARGE. An unrecognised value falls back to MEDIUM

Required Integration: DynamicMenuAuthorityUtils

Dynamic Menu requires an implementation of DynamicMenuAuthorityUtils to know what roles exist in the application. This information is used both in the permissions editor (to populate the role selection dropdowns) and at runtime (to evaluate which menu items the current user is allowed to see).

The Getting Started guide shows a minimal implementation using a fixed set. In production applications that use User Manager, the implementation should delegate to AuthorityService to return roles dynamically:

@Primary
@Component
public class AppDynamicMenuAuthorityUtils implements DynamicMenuAuthorityUtils {

    @Autowired
    private AuthorityService authorityService;

    @Override
    public Set<String> getAvailableAuthorities() {
        return authorityService.findAll().stream()
            .map(AuthorityDto::getName)
            .collect(Collectors.toSet());
    }

    @Override
    public String getAnonymousAuthority() {
        return authorityService.findAnonymous()
            .map(AuthorityDto::getName)
            .orElse("");
    }
}

The @Primary annotation is required to override the default implementation provided by the module. Without it, Spring will raise an ambiguity error if both beans are present.

Rendering the Menu

DynamicMenuItemProvider is a Spring-managed bean that reads the stored menu items, filters them by the current user's roles, and converts them to Vaadin SideNavItem instances ready for rendering. Inject it into the application's MainLayout to drive navigation entirely from the database:

@Autowired
private DynamicMenuItemProvider dynamicMenuItemProvider;

private SideNav createNavigation() {
    SideNav nav = new SideNav();
    SideNavItem[] items = dynamicMenuItemProvider.getMenuItems();
    nav.addItem(items);
    return nav;
}

getMenuItems() resolves the current user's roles from the active HTTP session and returns the items that survive the rendering pipeline described below. For an unauthenticated request it uses the single authority returned by getAnonymousAuthority().

The bean is annotated @Primary and implements DefaultItemProvider, so an application built on the AppJars layout picks it up as the source of its navigation without further wiring. It is also reachable statically through DynamicMenuItemProvider.getInstance().

Note

While the menu item table is empty, getMenuItems() returns a single hard-coded Menu Items entry pointing at the management view. Without it a fresh installation would render an empty drawer, leaving no way to reach the editor and create the first item.

MenuItemFactory turns the stored items into SideNavItem components. Between reading the items and building the components it applies three filtering passes, in this order.

flowchart TD
    A[Stored menu items] --> B[Security constraint matching]
    B --> C[Route access filtering]
    C --> D[Empty group pruning]
    D --> E[Separator visibility rules]
    E --> F[SideNavItem components]
Hold "Alt" / "Option" to enable pan & zoom

Security constraint matching

Each menu item can carry an optional security constraint. It is evaluated against the current user's set of granted roles using three independent rule sets:

Rule set Table Behaviour
If all granted dm_sc_all_granted The item is visible only if the user holds every role in this set
If any granted dm_sc_any_granted The item is visible only if the user holds at least one role in this set
If not granted dm_sc_not_granted The item is hidden if the user holds any role in this set

All populated rule sets must pass simultaneously, and an empty rule set always passes. An item with no constraint attached is always visible.

Route access filtering

Items flagged with onlyRenderIfAllowed ignore their security constraint entirely. Instead, their target route is resolved and checked with Vaadin's AccessAnnotationChecker, so the entry appears only when the current user could actually navigate to the view. This keeps the menu consistent with the access annotations already declared on the views, with no per-item configuration.

The check only applies to items with an internal URL. Items with an external URL, separators, and group headers without a destination of their own are always kept. If a flagged item points at a route that no longer exists, the item is dropped and a warning is logged naming the item and the missing route.

Empty group pruning

Filtering can leave a grouping container with nothing in it. A pure group — an item that is not a separator and has no URL of its own — that had children before filtering but none afterwards is removed. Pruning propagates upward, so a group whose only child was itself a pruned group is removed as well. Groups that never had children are left alone, since they were not acting as containers.

Separator visibility rules

The last pass is presentational, and is applied at every nesting level:

  • A separator without a label is a plain divider. It is kept only if the nearest entry kept before it is a non-separator item and at least one non-separator item follows it. Leading, trailing and consecutive dividers are therefore dropped.
  • A separator with a label is a section header. It is kept only if a non-separator item follows it before the next separator or the end of the list. Sitting at the start of the list is fine.

For internationalised separators the decision is made on the raw label key, without resolving translations.

Service API

MenuItemService manages the full menu item hierarchy. Beyond standard CRUD, it exposes the following methods:

// Return top-level items whose security constraints match the given roles
List<MenuItemDto> getAllowedMenuItems(Set<String> userCredentials);

// Return all direct children of a parent item
List<MenuItemDto> getItemsByParent(MenuItemDto parent);

// Return all top-level items ordered by their index
List<MenuItemDto> getFirstLevelItems();

// Reorder items via drag-and-drop (ON_TOP, ABOVE, BELOW, EMPTY)
void moveItemToNewLocation(MenuItemDropLocation location, MenuItemDto target,
                           MenuItemDto parent, MenuItemDto dragged);

// Remove an item and promote its children to the item's parent level
void deleteKeepChildren(MenuItemDto item);

// Move an item inside another (validates no external-URL parents)
void moveInside(MenuItemDto target, MenuItemDto toMove);

// Reorder a top-level item among its siblings
void moveAbove(MenuItemDto target, MenuItemDto toMove);
void moveBelow(MenuItemDto target, MenuItemDto toMove);

// Make an item a child of parent and place it among the parent's children
void moveAbove(MenuItemDto parent, MenuItemDto target, MenuItemDto toMove);
void moveBelow(MenuItemDto parent, MenuItemDto target, MenuItemDto toMove);

// Detach an item from its parent and promote it to the top level
void removeParent(MenuItemDto item);

// Total number of stored menu items, at every level
long countAll();

Sibling order is held in the item_index column and is managed by the service: a saved item takes the highest index among its siblings plus one, and the move operations reindex only the sibling group they affect. deleteKeepChildren appends the promoted children after the entries already at the target level.

Parent-child relationships enforce two constraints: items with an external URL cannot have children, because external links cannot serve as navigational containers, and an item cannot be moved inside one of its own descendants. Both are reported as validation errors.

Import and Export

The menu hierarchy can be exported to JSON and re-imported, which is useful for migrating configurations between environments or seeding a fresh database.

Export serialises the full hierarchy (including security constraints and URL parameters) to a JSON string:

String json = menuItemService.exportMenuItems();

Import accepts a JSON string and a strategy that controls how existing items are handled:

List<MenuItemDto> parsed = menuItemService.parseMenuItems(json); // preview, no save
menuItemService.saveImportedMenuItems(parsed, ImportStrategy.OVERWRITE);
Strategy Behaviour
REMOVE_ALL Delete all existing items, then insert the imported set
OVERWRITE Update items that share an ID with imported entries; create the rest
ADD_NEW Ignore items that share an ID with existing entries; create only new ones
ALL_AS_NEW Discard the IDs in the payload and insert every item as a new one, leaving existing items untouched

parseMenuItems validates the payload before anything is written and throws a ValidationException if an item carries both an internal and an external URL, or has no label. The serialised form covers every persisted attribute, iconSize and onlyRenderIfAllowed included, so an export taken from one environment reproduces the same rendering behaviour in another.

Customisation

Assigning a Router Layout

By default, Dynamic Menu registers its view without a parent layout. To wrap it in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:

@Autowired
@Qualifier("DynamicMenuRouteConfigurer")
private RouteConfigurer routeConfigurer;

@PostConstruct
public void configure() {
    routeConfigurer.setViewsRouterLayout(MainLayout.class);
}

The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.

Customising the View URL and Icon Size

Both the management view URL and the default icon size used in navigation can be overridden in application.properties:

com.appjars.dynamicmenu.url.views.menuitemsview=myapp/menu-items
appjars.dynamicmenu.icon.size=LARGE

The size property sets the application-wide default only. An item that carries its own Icon Size keeps it, whatever the default is.

Offering Additional Icon Families

The icon families offered in the menu item editor come from an IconFamilyProvider bean:

public interface IconFamilyProvider {
  List<IconFamily> getIconFamilies();
}

// factory: an enum implementing com.vaadin.flow.component.icon.IconFactory
// displayName: the label shown in the Icon Type selector
public record IconFamily(Class<? extends IconFactory> factory, String displayName) {}

A menu item stores its icon as two values: the name of the enum constant, and the binary name of the enum that provides it. The icon is rebuilt generically at render time, so the appjar is not tied to any particular icon set — any enum implementing IconFactory works as a family.

The default provider exposes only the Vaadin icon set. To offer more, declare a @Primary bean of your own:

@Configuration
public class IconProviderConfiguration {

  @Bean
  @Primary
  public IconFamilyProvider iconFamilyProvider() {
    return () -> List.of(
        new IconFamily(VaadinIcon.class, "Vaadin Icons"),
        // requires the com.flowingcode.addons:font-awesome-iron-iconset dependency
        new IconFamily(FontAwesome.Solid.class, "FontAwesome Solid"),
        new IconFamily(FontAwesome.Brands.class, "FontAwesome Brands"),
        // your own enum implementing IconFactory
        new IconFamily(MyCustomIcons.class, "Custom Icons"));
  }
}

Note

FontAwesome is not bundled with this appjar. An application that wants FontAwesome icons must add the com.flowingcode.addons:font-awesome-iron-iconset dependency itself and include those families in its provider.

For a fully custom set, implement the enum and register the matching vaadin-iconset in the frontend so that the icon names resolve:

public enum MyCustomIcons implements IconFactory {
  ROCKET("custom:rocket"),
  FLOW("custom:flow");

  private final String icon;

  MyCustomIcons(String icon) {
    this.icon = icon;
  }

  @Override
  public Icon create() {
    return new Icon(icon); // "collection:name" of a registered Vaadin icon set
  }
}

Changing the families an application offers does not migrate the items already stored. An item whose iconType names a family that is no longer available cannot be rebuilt: the management grid and the import preview leave its icon cell empty, and the import preview clears the icon from the item rather than failing the import.