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 if_all_granted
    }
    dm_sc_any_granted {
        integer security_constraint_entity_id FK
        varchar if_any_granted
    }
    dm_sc_not_granted {
        integer security_constraint_entity_id FK
        varchar if_not_granted
    }
    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),
    CONSTRAINT uk_dm_menu_item_permissions UNIQUE (permissions_id)
);

CREATE TABLE dm_sc_all_granted (
    security_constraint_entity_id INTEGER      NOT NULL,
    if_all_granted                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,
    if_any_granted                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,
    if_not_granted                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). That column is also unique: an item owns at most one security constraint and a constraint is never shared between items. 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.

The icon_size column stores the name of the size constant. The DDL above declares it as a plain VARCHAR, which is what most dialects produce; a dialect with native enumerated types, such as H2, emits its own enum type for the same mapping.

Each role collection table names its value column after the attribute it stores, so the three carry if_all_granted, if_any_granted and if_not_granted rather than one shared column name.

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.

Applications should compile against the contracts in the model, data, business and flow artifacts. The classes inside -data-impl and -business-impl are the standard runtime implementations, not supported extension points: some of them are Java-public only so that the framework can discover them, and they may change without a major version increment. Depend on the DAO and service interfaces and on the model DTOs instead.

Note

Version 2.0 assigned explicit serial version identifiers to the serializable DTOs and to the Vaadin session-state types. Sessions, caches or messages serialised by an earlier version cannot be carried across the upgrade.

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.

Driving the Editor Programmatically

The management view and the item editor expose a small set of methods so that an application embedding them can open the editor without reaching into the component tree:

// ListMenuItemsView
void openItemDialog();      // opens the editor in creation mode, as the New item button does
void openItemPermissions(); // opens it in creation mode, with the Permissions tab selected
void closeItemDialog();     // closes the editor if it is open

// MenuItemDialog
void showPermissionsTab();  // selects the Permissions tab of an already open editor

These are the supported way to script the editor — from a guided product tour, for instance — and they keep working across changes to the internal layout of the view.

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.

Testing Against the Views

The Dynamic Menu views expose stable data-testid attributes so that integration tests can select elements without depending on the DOM structure or on translated labels. The identifiers are declared as constants in com.appjars.dynamicmenu.flow.view.util.TestIds and cover the grid, the New item button, the global actions menu, the free-mode restrictions bar, the empty-grid message, the fields and tabs of the item editor, and the import dialog.

Selecting by these attributes is the supported way to drive the views from a test, and keeps tests unaffected by layout or wording changes.

Note

The host element of a Vaadin dialog is not rendered, so only the components inside the item editor and the import dialog carry an identifier, not the dialogs themselves.