Skip to content

reading a reflexive association in a recursive way for grails and groovy

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.

Categories: algorithms, grails, groovy.

Tags: , , , , , ,

Comment Feed

No Responses (yet)



Some HTML is OK

or, reply to this post via trackback.