grailsgrails-filters

How do I DRY out common logic from a grails filter?


This is specifically about a grails 1.3.7 application, but hopefully the answers will work for newer versions as well. The code below is a simplified version of what is needed. accountService is being injected. The below snippet does what it's supposed to do but clearly is repeated code. This is in a UserFilter class located in grails-app/conf

How do I extract common logic out of my filters and maintain the capability to redirect and check the session? I've tried extracting out a method into the filter class, passing in the session and flash, but the redirect was still giving me issues.

def filters = {
  // ... other filters ...
  adminAllCheck(controller: 'administration', action: '*') {
    before = {
      if(!session.isAdmin) {
        if(accountService.isAdmin()) {
          session.isAdmin = true
        } else {
          flash.message = 'Non admin'
          redirect(controller: 'home', action: 'index')
          return false
        }
      }
      true
    }
  }
  userListCheck(controller: 'user', action: 'list') {
    before = {
      if(!session.isAdmin) {
        if(accountService.isAdmin()) {
          session.isAdmin = true
        } else {
          flash.message = 'Non admin'
          redirect(controller: 'home', action: 'index')
          return false
        }
      }
      true
    }
  }
}    

Solution

  • One way to create a helper method is to create it outside of the filters closure, and pass the instance in. It doesn't work to pass in this because that's not the closure but the UserFilters instance. Instead pass in the closure's delegate which is where the render and redirect methods are added, where the attributes like params and controllerName are:

    class UserFilters {
       def filters = {
          // ... other filters ...
          adminAllCheck(controller: 'administration', action: '*') {
             before = {
                doAdminCheck(delegate)
             }
          }
          userListCheck(controller: 'user', action: 'list') {
             before = {
                doAdminCheck(delegate)
             }
          }
       }
    
       private boolean doAdminCheck(filters) {
          if (!filters.session.isAdmin) {
             if (accountService.isAdmin()) {
                filters.session.isAdmin = true
             }
             else {
                filters.flash.message = 'Non admin'
                filters.redirect(controller: 'home', action: 'index')
                return false
             }
          }
          true
       }
    }
    

    You can also use the | character in the controller and action arguments to do common work across controllers. It wouldn't work here directly since you use * for the admin controller and only apply to the list action in the user controller, but you can do an explicit controller/action name check for that:

    adminCheck(controller: 'administration|user', action: '*') {
       if (controllerName == 'user' && actionName != 'list') {
          return true
       }
       // common logic here
    }
    

    You could also move the logic to a service and dependency-inject that back in. It's usually not a good idea to mix tiers like that and have HTTP logic in a service, but that would keep the logic in one place. You could use the same trick with the delegate, or just pass in the session/request/response/etc. as needed.