Index: trunk/src/groovy/HqlBuilder.groovy
===================================================================
--- trunk/src/groovy/HqlBuilder.groovy	(revision 641)
+++ trunk/src/groovy/HqlBuilder.groovy	(revision 642)
@@ -16,6 +16,10 @@
 /**
  * Provides a DSL for building and managing HQL strings.
- * For usage examples see the HqlBuilderTests.
+ * For more usage examples see the HqlBuilderTests.
  * HQL reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/queryhql.html
+ *
+ * DML reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/batch.html#batch-direct
+ * DML-style clauses: "update, delete, insert into" may not be well tested, the "set" clause has basic implementation.
+ * So double check the q.query output for these cases.
  *
  * Primary goals:
@@ -25,12 +29,26 @@
  * 4. Easy to change a clause and execute again.
  *
+ * Basic usage:
+ *    def q = new HqlBuilder().query {
+ *        select 'count(distinct book)'
+ *        from 'Book as book'
+ *        where 'book.id > 100'
+ *            and 'book.inStock = true'
+ *    }
+ *    def totalCount = Book.executeQuery(q.query, q.namedParams, q.paginateParams)[0].toInteger()
+ *    q.select = 'distinct book'
+ *    def list = Book.executeQuery(q.query, q.namedParams, q.paginateParams)
+ *
+ *    def bookList = new PagedResultList(list, totalCount)
+ *    log.debug '\n' + q.printFormattedQuery
+ *
  * @author Gavin Kromhout
- * @version DraftA
+ * @version DraftB
  *
  */
 class HqlBuilder {
 
-    // Query clauses.
-    // Each clause is a key with a list of terms.
+    // HQL clauses.
+    // Each clause is a map key with a list of terms.
     def clauses = [:]
 
@@ -42,14 +60,47 @@
     def paginateParams = [max: 1000, offset: 0]
 
-    // It is easier and more flexible to simply add order as a clause, e.g: order 'by id asc'
-    //def sort = "" // e.g. instanceName.id
-    //def order = "" // e.g. asc or desc
-
-    def HqlBuilder(debug = false) {
+    // The where clause terms are handled separately from other clauses
+    // and are a list of logicalTerms.
+    // The where clause is built by buildWhereClause.
+    def whereClauseTerms = []
+
+    // LogicalIndexStack holds indexes of the current whereClauseTerm nesting.
+    def logicalIndexStack = []
+
+   def logicalBuilders = [AND: 'and',
+                                        OR: 'or']
+
+    def nestingStack = []
+
+    // Sort and Order.
+    // It is easier and more flexible to simply add order as a clause, e.g: order 'by name desc, id asc'
+    // def sort = "" // e.g. instanceName.id
+    // def order = "" // e.g. asc or desc
+
+    /**
+     * Constructor.
+     * Any property that exists (or responds) in the class may be supplied as an argument.
+     * E.g: max:20, offset:10, debug:true
+     * The debug property does not really exist, but if true and no external log property
+     * has been setup then the internal mockLogger will be configured in debug mode.
+     *
+     * @param args A map of arguments, defaults to an empty map.
+     *
+     */
+    def HqlBuilder(Map args = [:]) {
+        args.each { arg ->
+            def argKey = arg.key.toLowerCase()
+            if(super.hasProperty(argKey))
+                this[argKey] = arg.value
+        }
         if(!super.metaClass.hasMetaProperty('log'))
-            mockLogging(debug)
+            mockLogging(args.debug)
         log.debug "HqlBuilder()"
     }
 
+    /**
+     * Call with no args.
+     *  Has no real use other than to prevent obscure errors.
+     */
     def call() {
         log.debug "call()"
@@ -69,5 +120,7 @@
      *  Has no real use other than to prevent obscure errors
      * when user makes a call to query() and Groovy calls query(Closure cl)
+     *
      * @returns This object.
+     *
      */
     def query() {
@@ -78,7 +131,7 @@
     /**
      * Domain specific build method.
-     * A typically used build call, e.g: def q = new HqlBuilder().query { }
-     *
-     * @param cl The supplied Closure.
+     * The recommended build call, e.g: def q = new HqlBuilder().query { }
+     *
+     * @param cl The closure that will be used to build the query.
      * @returns This object.
      *
@@ -92,23 +145,33 @@
     /**
      * InvokeMethod resolves all undefined methods.
-     * Which include the clause methods, e.g select 'book' is equivalent to select('book').
+     * Which include the clause methods, e.g: select 'book' is equivalent to select('book').
      * Note that defined methods will be called directly since this class does not implement GroovyInterceptable.
      * If class was "HqlBuilder implements GroovyInterceptable" then even println would be intercepted and
      * several exlusions might be needed. e.g: if(methodName != 'call' && methodName != 'println')
      */
-    def invokeMethod(String name, args) {
+    def invokeMethod(String methodName, args) {
 
         log.debug "invokeMethod(${methodName}, ${args})"
 
-        if(!this.clauses[name])
-            this.clauses[name] = []
+        // Call any closures first, that way the nesting is handled and we just keep a reference.
+        if(args[-1] instanceof Closure) {
+            handleClosure(args[-1], methodName)
+            args = args.minus(args[-1])
+        }
+
+        if(!clauses.containsKey(methodName) && !isLogicalBuilder(methodName))
+            clauses[methodName] = []
+
+        if(args) {
+            if(isWhereClauseBuilder(methodName)) {
+                logicalBuilder(methodName, args)
+                return
+            }
+        }
 
         for(arg in args) {
-            if(arg instanceof String)
-                this.clauses[name] << arg
-        }
-
-        if(args[-1] instanceof Closure)
-            handleClosure(args[-1])
+            if(arg instanceof String || arg instanceof GString)
+                clauses[methodName] << arg
+        }
 
     } // invokeMethod()
@@ -119,14 +182,27 @@
      * and clauses to be removed, e.g: q.order = null
      */
-    def propertyMissing(String name, value) {
+    def propertyMissing(String propertyName, value) {
         log.debug "propertyMissing(${propertyName}, ${value})"
+
         if(value == null) {
-            clauses.remove(name)
+            removeClause(propertyName)
+            if(propertyName.toLowerCase() == 'where')
+                whereClauseTerms.clear()
             return
         }
 
-        if(value instanceof String)
-            clauses[name] = [value]
-    }
+        if(!clauses.containsKey(propertyName))
+            clauses[propertyName] = []
+
+        // Occurs when user assigns to where clause, e.g: q.where = 'book.id > 100'
+        if(propertyName.toLowerCase() == 'where') {
+            whereClauseTerms.clear()
+            logicalBuilder(propertyName, [value])
+            return
+        }
+
+        if(value instanceof String || value instanceof GString)
+            clauses[propertyName] = [value]
+    } // propertyMissing(String propertyName, value)
 
     /**
@@ -135,7 +211,155 @@
      * Since clauses is a Map null is simply returned for a non-existant clause.
      */
-    def propertyMissing(String name) {
-        log.debug "propertyMissing(${name})"
-        clauses[name]
+    def propertyMissing(String propertyName) {
+        log.debug "propertyMissing(${propertyName})"
+
+        if(!clauses.containsKey(propertyName))
+            clauses[propertyName] = []
+
+        // Occurs when user performs an operation on where clause.
+        // E.g: q.where << "book.id = 100" which is actually NOT a supported operation since
+        // calling the method provides the correct function e.g: q.where "book.id > 100".
+        // Also allows `println q.where` to be short hand for `println q.whereClauseTerms`
+        if(propertyName.toLowerCase() == 'where') {
+            return whereClauseTerms
+        }
+
+        clauses[propertyName]
+    } // propertyMissing(String propertyName)
+
+    def setMax(Integer value) {
+        paginateParams.max = value
+    }
+
+    def getMax() {
+        paginateParams.max
+    }
+
+    def setOffset(Integer value) {
+        paginateParams.offset = value
+    }
+
+    def getOffset() {
+        paginateParams.offset
+    }
+
+    /**
+     * RemoveClause.
+     * Allows clauses to be removed, e.g: q.removeClause('order')
+     *
+     * @param clauseName The clause to remove.
+     *
+     */
+    def removeClause(String clauseName) {
+            clauses.remove(clauseName)
+    }
+
+    /**
+     * BuildWhereClause.
+     * Build the where clause from whereClauseTerms.
+     */
+    def buildWhereClause(printFormat = false) {
+        //log.debug "buildWhereClause()"
+
+        if(!whereClauseTerms)
+            return ''
+
+        def whereClause = 'where '
+
+        def buildExpression // declared separately to allow recurrsion.
+        buildExpression = { term ->
+            def result = ''
+            def termCount = term.expressions.size()
+            if(termCount > 1) {
+                term.expressions.eachWithIndex { t, index ->
+                    if(index == 0)
+                        result += buildExpression(t)
+                    else if(printFormat)
+                        result += " \n\t${t.logic} ${buildExpression(t)}"
+                    else
+                        result += " ${t.logic} ${buildExpression(t)}"
+
+                }
+                result = "( "+result+" )"
+            }
+            else {
+                if(term.expressions[0] instanceof Map)
+                    result += "${term.expressions[0].expressions[0]}"
+                else
+                    result += "${term.expressions[0]}"
+            }
+            return result
+        }
+
+        whereClauseTerms.eachWithIndex { tm, index ->
+            if(index == 0)
+                whereClause += buildExpression(tm)
+            else if(printFormat)
+                whereClause += " \n\t${tm.logic} ${buildExpression(tm)}"
+            else
+                whereClause += " ${tm.logic} ${buildExpression(tm)}"
+        }
+
+        return whereClause
+    } // buildWhereClause(printFormat = false)
+
+    /**
+     * LogicalBuilder.
+     * Build the whereClauseTerms
+     * by appending logicalTerms to the appropriate expressions.
+     */
+    def logicalBuilder(logicalName, args) {
+        log.debug "logicalBuilder(${logicalName}, ${args})"
+        log.debug "logicalIndexStack: ${logicalIndexStack}"
+
+        def logic = getLogicalString(logicalName)
+
+        for(arg in args) {
+            if(arg instanceof String || arg instanceof GString) {
+                arg = arg.trim()
+                if(arg) { // prevent empty strings being added.
+                    if(logicalIndexStack.size() > 0) {
+                        // Append to current index position.
+                        whereClauseTerms[logicalIndexStack[-1]].expressions << logicalTerm(logic, arg)
+                    }
+                    else {
+                        // Append to 'root'.
+                        whereClauseTerms << logicalTerm(logic, null) // empty expression logicalTerm.
+                        whereClauseTerms[-1].expressions << logicalTerm(logic, arg) // append logicalTerm to expressions
+                    }
+                } // if(arg)
+            } // if(arg instanceof)
+        } // for
+
+    } // logicalBuilder(logicalName, args)
+
+    /**
+     * LogicalTerm.
+     * A logicalTerm is a map object that holds the logic and list of expressions of a whereClauseTerm.
+     */
+    def logicalTerm = { logic, expression ->
+        expression = expression ? [expression] : []
+        ['logic': getLogicalString(logic), 'expressions': expression]
+    }
+
+    /**
+     * GetLogicalString.
+     *
+     * @param logicalName The name to get the matching logicalBuilder string for.
+     */
+    private getLogicalString(logicalName) {
+
+        switch(logicalName.toLowerCase()) {
+            case 'where':
+                logicalBuilders.AND
+                break
+            case logicalBuilders.AND:
+                logicalBuilders.AND
+                break
+            case logicalBuilders.OR:
+                logicalBuilders.OR
+                break
+        }
+
     }
 
@@ -144,8 +368,19 @@
      * Setting delegate and DELEGATE_FIRST allows closure to access this object's properties first.
      */
-    private handleClosure(Closure cl) {
+    private handleClosure(Closure cl, String methodName = 'root') {
+        log.debug "handleClosure(${cl.toString()}, ${methodName})"
+        if(isWhereClauseBuilder(methodName)) {
+            whereClauseTerms << logicalTerm(getLogicalString(methodName), null)
+            logicalIndexStack << whereClauseTerms.size()-1
+        }
+        nestingStack.push(methodName)
         cl.delegate = this
         cl.resolveStrategy = Closure.DELEGATE_FIRST
         cl.call()
+        //log.debug "nestingStack: $nestingStack"
+        nestingStack.pop()
+        if(isWhereClauseBuilder(methodName)) {
+            logicalIndexStack.pop()
+        }
     }
 
@@ -166,4 +401,24 @@
 
     /**
+     * IsLogicalBuilder.
+     * Determine if a method is a logicalBuilder.
+     */
+    private isLogicalBuilder(String methodName) {
+        logicalBuilders.find{ it.value == methodName.toLowerCase()} ? true:false
+    }
+
+    /**
+     * IsWhereClauseBuilder.
+     * Determine if a method is a where clause builder.
+     */
+    private isWhereClauseBuilder(String methodName) {
+        methodName = methodName.toLowerCase()
+        if(methodName == 'where' || isLogicalBuilder(methodName))
+            return true
+        else
+            return false
+    }
+
+    /**
      * GetQuery.
      * Assemble and return the query in a format that can be directly executed.
@@ -171,8 +426,26 @@
      */
     def getQuery() {
-        this.clauses.collect {
-            it.key + ' ' + it.value.join(' ')
+        clauses.collect { clause ->
+            switch (clause.key.toLowerCase()) {
+                case 'select':
+                    clause.key + ' ' + clause.value.join(', ')
+                    break
+                case 'set':
+                    clause.key + ' ' + clause.value.join(', ')
+                    break
+                case 'where':
+                    buildWhereClause()
+                    break
+                case 'order':
+                    clause.key + ' ' + clause.value.join(', ')
+                    break
+                case 'group':
+                    clause.key + ' ' + clause.value.join(', ')
+                    break
+                default:
+                    clause.key + ' ' + clause.value.join(' ')
+            }
         }.join(' ')
-    }
+    } // getQuery()
 
     /**
@@ -182,8 +455,26 @@
      */
     def getPrintFormattedQuery() {
-        this.clauses.collect {
-            it.key + ' ' + it.value.join(' \n')
+        clauses.collect { clause ->
+            switch (clause.key.toLowerCase()) {
+                case 'select':
+                    clause.key + ' ' + clause.value.join(', \n\t')
+                    break
+                case 'set':
+                    clause.key + ' ' + clause.value.join(', \n\t')
+                    break
+                case 'where':
+                    buildWhereClause(true)
+                    break
+                case 'order':
+                    clause.key + ' ' + clause.value.join(', \n\t')
+                    break
+                case 'group':
+                    clause.key + ' ' + clause.value.join(', \n\t')
+                    break
+                default:
+                    clause.key + ' ' + clause.value.join(' \n\t')
+            }
         }.join(' \n')
-    }
+    } // getPrintFormattedQuery()
 
 } // end class
