Monday, April 25, 2011

A quick journey through Grails searchable plugin

Grails searchable plugin is a great Compass based tool to implement search in Grails. As a part of my server inventory app, I used it to implement Ajax based search where search results update as you type. The relevant part of my domain model looks like this:

class BaseDomain {
    Date dateCreated
    Date lastUpdated
    Boolean active = true
}

class Host extends BaseDomain {
    String ipAddress
    String hostName
    Site site
}

class Site extends BaseDomain {
    String name
    String description
}

Objective: user searches for matching hosts by either IP address, host name, site name, as well as filter by active/inactive field.


Defining searchable fields.

class Host extends BaseDomain {
    static searchable = {
        only: ["ipAddress", "hostName", "site", "active"]
        ipAddress boost: 2.0
        hostName boost: 2.0 
        site component: true
    }

    String ipAddress
    String hostName
    Site site
}

class Site extends BaseDomain {
    static searchable = { 
        only: ["name", "active"]
    }

    String name
    String description
}

"ipAddress" and "hostName" get a boost over "active" and Site.name. Site is defined as searchable component, meaning if a match is found on a site name, the Host that has a Site whose name matched the query is returned in the search result.


Working the query

User submitted query string should be a wildcard match on Host.ipAddress and Host.hostName fields, so if we have following data:

ip address 127.0.0.1
host name localhost
active true
site name yahoo

queries like '127.0' or '0.0.1', 'local', 'calho' , 'yahoo', should all produce the above Host object as a match. Since there is only one search field, there is no way to know if what a user submitted is an ip address, a host name, or a site, therefore, all fields need to be checked. So, if a user submits "127.0", the query string for search should look like this:

ipAddress:*127.0* OR hostName:*127.0*

Since Site is a component, its matches are to be handled separately inside the search closure:

def wildcardQuery = "*" + params.query + "*"
def searchQuery = "ipAddress:" + wildcardQuery + " OR hostName:" + wildcardQuery 
return Host.search({
    must {
        queryString(searchQuery)
        wildcard('$/Host/site/name', wildcardQuery)
    }
}, params)

Then we add search for only active Hosts unless 'inactive' flag is set:

return Host.search({
    must {
        queryString(searchQuery)
        wildcard('$/Host/site/name', wildcardQuery)
    }
    if (!params.inactive) {
        must(term('$/Host/active', "true"))
    }
}, params) 

Now, let's add sort to the query. Technically, nothing needs to be added to the above snippet as long as there is a params.sort in the request. It works fine for fields like "ipAddress" and "hostName". However, if sorting by Site.name or 'active', actual "sort" request parameter must contain '$/Host/site/name' and '$/Host/active', which is not very convenient. It is more practical to have "site" and "active" as the value for params.sort, clone params into a separate map, and then overwrite sort param with the right value. This is how entire search closure looks:

def search = { 
    def searchParams = [:]
    params.each {
        searchParams."$it.key" = it.value
    }

    switch (params.sort) {
        case "site" :
            searchParams.sort = '$/Host/site/name'
            break
        case "active" :
            searchParams.sort = '$/Host/active'
    }

    def wildcardQuery = "*" + searchParams.query + "*"
    def searchQuery = "ipAddress:" + wildcardQuery + " OR hostName:" + wildcardQuery 
    return Host.search({
        must {
            queryString(searchQuery)
            wildcard('$/Host/site/name', wildcardQuery)
        }
        if (!params.inactive) {
            must(term('$/Host/active', "true"))
        }
    }, searchParams)
}

Saturday, March 26, 2011

Sorting with an outer join using GORM

In a situation where a GORM query is to return a sorted result set, but the sort field belongs to the associated object, which may or may not be null, a regular Criteria query will not cut it, because of the following limitation:

Host.createCriteria().list {
    createAlias("site", "s")
    order("s.name", "asc")
}

Without "createAlias", the query would be an outer join on "site", but because we are trying to sort by site.name, "createAlias" makes the query an inner join, so all results where the site is null are lost. Using regular HQL would help us in this situation:

Host.findAll("from Host as h \
               left outer join h.site as s order by s.name asc") 

Monday, February 14, 2011

Reverse sort dropdown values in a GSP page

In a GSP page in a Grails application, it is easy to sort values in a dropdown in ascending order. Just pass a closure to sort() call with the name of the attribute to sort by.

  • ${i.name}

  • It is just as easy to sort in descending order. Just provide a sort closure with 2 parameters and return 1 or -1 as in a Comparator.compare() method.
    
    
  • ${i.name}
  • ... or see Tom's post below.

    Monday, January 31, 2011

    Extracting IP addresses from DiG output for a bunch of hosts

    In my Grails server inventory app, I had to account for future IP space refactoring, where a bunch of hosts would change their IP addresses. All users of the system will expect to see new IPs in all runbooks, so something had to be done to keep them in sync.

    So, I decided to add a Quartz job that would

    1. Query all existing hosts with a valid host name
    2. Call a 'dig' on that host
    3. Parse an IP address from dig response

    So, let's say we have a Host object that looks something like this:

    class Host {
        ...
        String hostName
        String ipAddress
        ...
    }
    

    Adding a Quartz job is a trivial task that can be easily accomplished using grails Quartz plugin. One advice: use the Quartz plugin instead of manually adding a TimerTask bean using resources.groovy. Apparently, Grails application's most vital aspects, such as autowiring of services and GORM Hibernate session are not available at construction time for beans defined in resources.groovy, because those beans are read before anything in grails-app directory. So, here's how my Quartz job looks in a Grails 1.3.4 application:

    import org.codehaus.groovy.grails.commons.ApplicationHolder
    
    class IpSynchJob {
        def hostService
        def startDelay
        def timeout
    
        public IpSynchJob() {
            startDelay = 0
            timeout = ApplicationHolder.application.config.timer.ipsynch.interval
        }
    
    
        def execute() {
            def hosts = Host.createCriteria().list {
                isNotNull("hostName")
            }
    
            hosts.each { host ->
                log.info "synching IP for hostname $host.hostName"
                synchIp(host)
            }
        }
    
        void synchIp(host) {
            def dig = digServer(host.hostName)
            def ipAddress = parseIpFromDigOutput(dig)
            host.ipAddress = ipAddress
            host.save()
        }
    
        String parseIpFromDigOutput(digOutput) {
    
            // looking for IP address in dig's answer section using positive lookahead
            def ipInDigPtn = /(ANSWER SECTION\:)(?=\n.*IN\s+A\s+(\d+\.\d+\.\d+\.\d+))/
            def ip
            digOutput.eachMatch(ipInDigPtn) { match ->
                ip = match[2]
            }
            return ip
        }
    
        String digServer(hostName) {
            Process p = "dig $hostName".execute()
            int initCapacity = 4096
            StringBuffer out = new StringBuffer(initCapacity)
            p.consumeProcessOutput(out, out)
            // maxing out wait at 20 seconds
            p.waitForOrKill(20000)
    
            return out.toString()
        }
    }
    

    Tuesday, January 4, 2011

    functional-test plugin is broken in Grails 1.3.x versions

    Got around to write some functional tests for the Grails app i'm working on. "Grails in Action" book recommends functional-test plugin, and it looks like it's pretty popular in the community, but it appears that it doesn't work for Grails 1.3.x versions. And since grails-plugins code hasn't been touched since July, it only makes sense to switch to something else at the moment.