Imagine you have a Menu with multiple items, those items have multiple items and so on. The Menu Items also depend on a Role a User has in the Application Context like ‘ROLE_ADMIN’, ‘ROLE_COOK’, ‘ROLE_SOME’ …
I think the best practice is to save the menu items as a reflexive association in a database table like this:
class MenuItem { Long id; String label; Boolean topLevel; Integer position; static hasMany = [roles:Role, childs:MenuItem] static belongsTo = [parent:MenuItem] static mapping = { topLevel type:'true_false' childs sort:'position' } static constraints = { } String toString() { "[MenuItem id: ${id}, label: ${label}, pos: ${position}]" } }
so lets initialize some dummy menu Items:
def file =new MenuItem(label:'file', position:1,topLevel:true,parent:null, roles:[[somerole,cookrole,adminrole]).save(); def view =new MenuItem(label:'view', position:2,topLevel:true,parent:null, roles:[cookrole,adminrole]).save(); def help =new MenuItem(label:'help', position:2,topLevel:true,parent:null, roles:[someroleadminrole]).save(); def logout =new MenuItem(label:'logout', position:2,topLevel:true,parent:null, roles:[somerole,cookrole,adminrole]).save();
those 4 toplevel MenuItems have different roles assiciated with them, for example the view menu can only be accessed by a user wich has the cookrole or the adminrole. the roles have also the property of a level as a number which is zero for an admin user and one for a role which has less rights than the admin. the algorithm then extracts the hightest role from a user instance.
so lets insert some submenus to the database table:
new MenuItem(label:'save File', position:1,topLevel:false,parent:file, roles:[[somerole,cookrole,adminrole]).save(); new MenuItem(label:'save All Files', position:2,topLevel:false,parent:file, roles:[[somerole,cookrole,adminrole]).save(); def serivces = new MenuItem(label:'services', position:3,topLevel:false,parent:file, roles:[[somerole,cookrole,adminrole]).save(); new MenuItem(label:'capture Image', position:1,topLevel:false,parent:serivces, roles:[[somerole,cookrole,adminrole]).save(); new MenuItem(label:'send As Email', position:2,topLevel:false,parent:serivces, roles:[[somerole,adminrole]).save();
notice that the last menu item (send As Email) is not accessible for the cookrole
We now have a three dimensional menu like this:
- file
- save File
- save All Files
- services
- capture Image
- send As Email
- view
- help
- logout
here is the code to render this menu structure in a controller and send it to the view:
class MainController { def authenticateService def testUser = null def menuItems = [:] void populateMenuItems(map,hightestRole){ //fill the first time with topLevel Items if(menuItems.isEmpty()){ map.each({ if(it.roles.contains(hightestRole)) menuItems[it] = [:] }) } menuItems.each({ mi -> MenuItem.findAllByParent(mi.key).each({ subMi -> if(subMi.roles.contains(hightestRole)){ mi.value[subMi] = [:] findMore( mi.value[subMi],subMi) } }) }) } void findMore(mapEntry,mi){ def moreItems = MenuItem.findAllByParent(mi) if(!moreItems.isEmpty()){ moreItems.each({ mapEntry[it] = [:] findMore(mapEntry[it],it) //recursive call }) } } def index = { def hightestRole = null def principal = (testUser != null)? testUser : authenticateService.principal(); def itRole principal.getAuthorities().each({ itRole = Role.findByAuthority(it.authority) if(hightestRole == null || ( hightestRole != null && itRole.level < hightestRole.level)) hightestRole = itRole }) populateMenuItems( MenuItem.findAllByTopLevel( true, [sort: 'position', order: 'asc'] ), hightestRole ) return [ highestRole:hightestRole, menuItems:menuItems, user: principal ] } }
the function populateMenuItems calls the function findMore which calls itself recursively as long as a submenu for some item exists.
here are some integation tests for this:
def miSaveFile = MenuItem.findByLabel('save File') def miSendAsEmail = MenuItem.findByLabel('send As Email') def user = User.get(2) assertEquals "bestcook", user.username controller.testUser = user; def model = controller.index() assertEquals "bestcook", model["bestcook"]?.username assertEquals Role.findByAuthority("ROLE_COOK"),model["highestRole"] assertFalse SearchNestedHash.search(model, miSendAsEmail) assertTrue SearchNestedHash.search(model, miSaveFile)
the SearchNestedHash class is explained in my previous article.












