Index: /trunk/src/groovy/HqlBuilder.groovy
===================================================================
--- /trunk/src/groovy/HqlBuilder.groovy	(revision 641)
+++ /trunk/src/groovy/HqlBuilder.groovy	(revision 641)
@@ -0,0 +1,189 @@
+/* Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides a DSL for building and managing HQL strings.
+ * For usage examples see the HqlBuilderTests.
+ * HQL reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/queryhql.html
+ *
+ * Primary goals:
+ * 1. Easy to read and understand in code.
+ * 2. Easy to read and understand when printed (e.g when displayed in a report).
+ * 3. Easy to execute with correct paginateParams and namedParams.
+ * 4. Easy to change a clause and execute again.
+ *
+ * @author Gavin Kromhout
+ * @version DraftA
+ *
+ */
+class HqlBuilder {
+
+    // Query clauses.
+    // Each clause is a key with a list of terms.
+    def clauses = [:]
+
+    // HQL namedParams.
+    // HQL requires the namedParams to match exactly with the clause expressions.
+    def namedParams = [:]
+
+    // HQL paginateParams.
+    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) {
+        if(!super.metaClass.hasMetaProperty('log'))
+            mockLogging(debug)
+        log.debug "HqlBuilder()"
+    }
+
+    def call() {
+        log.debug "call()"
+    }
+
+    /**
+     * Call with closure as last arg.
+     * A typically used build call, e.g: q { } is equivalent to q.call() { }
+     */
+    def call(Closure cl) {
+        log.debug "call(Closure cl)"
+        handleClosure(cl)
+    }
+
+    /**
+     * Domain specific build method.
+     *  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() {
+        log.debug "query()"
+        return this // Must return this object to q.
+    }
+
+    /**
+     * Domain specific build method.
+     * A typically used build call, e.g: def q = new HqlBuilder().query { }
+     *
+     * @param cl The supplied Closure.
+     * @returns This object.
+     *
+     */
+    def query(Closure cl) {
+        log.debug "query(Closure cl)"
+        handleClosure(cl)
+        return this // Must return this object to q.
+    }
+
+    /**
+     * InvokeMethod resolves all undefined methods.
+     * 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) {
+
+        log.debug "invokeMethod(${methodName}, ${args})"
+
+        if(!this.clauses[name])
+            this.clauses[name] = []
+
+        for(arg in args) {
+            if(arg instanceof String)
+                this.clauses[name] << arg
+        }
+
+        if(args[-1] instanceof Closure)
+            handleClosure(args[-1])
+
+    } // invokeMethod()
+
+    /**
+     * PropertyMissing.
+     * Allows clauses to be added after build, e.g: q.order = 'by book.name asc'
+     * and clauses to be removed, e.g: q.order = null
+     */
+    def propertyMissing(String name, value) {
+        log.debug "propertyMissing(${propertyName}, ${value})"
+        if(value == null) {
+            clauses.remove(name)
+            return
+        }
+
+        if(value instanceof String)
+            clauses[name] = [value]
+    }
+
+    /**
+     * PropertyMissing.
+     * Allow clauses to be accessed directly by name, e.g: println q.order.
+     * Since clauses is a Map null is simply returned for a non-existant clause.
+     */
+    def propertyMissing(String name) {
+        log.debug "propertyMissing(${name})"
+        clauses[name]
+    }
+
+    /**
+     * HandleClosure.
+     * Setting delegate and DELEGATE_FIRST allows closure to access this object's properties first.
+     */
+    private handleClosure(Closure cl) {
+        cl.delegate = this
+        cl.resolveStrategy = Closure.DELEGATE_FIRST
+        cl.call()
+    }
+
+    /**
+     * MockLogging.
+     * This class has super cow powers and can mock out it's own debug logging.
+     */
+    private mockLogging(debug = false) {
+        def mockLogger = {}
+        if(debug) {
+            mockLogger = {msg ->
+                    println "${super.getClass()} - DEBUG: $msg"
+            }
+        }
+        super.metaClass.log = [debug: mockLogger]
+        log.debug "Internal mockLogger configured."
+    }
+
+    /**
+     * GetQuery.
+     * Assemble and return the query in a format that can be directly executed.
+     * E.g: executeQuery(q.query, q.namedParams, q.paginateParams).
+     */
+    def getQuery() {
+        this.clauses.collect {
+            it.key + ' ' + it.value.join(' ')
+        }.join(' ')
+    }
+
+    /**
+     * GetPrintFormattedQuery.
+     * Assemble and return the query in a format that can be more easily printed and read by a person.
+     * E.g: println q.printFormattedQuery or when displayed in a report.
+     */
+    def getPrintFormattedQuery() {
+        this.clauses.collect {
+            it.key + ' ' + it.value.join(' \n')
+        }.join(' \n')
+    }
+
+} // end class
Index: /trunk/test/unit/HqlBuilderTests.groovy
===================================================================
--- /trunk/test/unit/HqlBuilderTests.groovy	(revision 641)
+++ /trunk/test/unit/HqlBuilderTests.groovy	(revision 641)
@@ -0,0 +1,123 @@
+/* Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.commons.logging.LogFactory
+
+/**
+ * Unit tests for HqlBuilder class.
+ * GroovyTestCase is used so that class does not depend on Grails as it may be useful outside of Grails.
+ *
+ * @author Gavin Kromhout
+ * @version DraftA
+ *
+ */
+public class HqlBuilderTests extends GroovyTestCase {
+
+    def n = '\n'
+    def savedMetaClass
+
+    protected void setUp() {
+        super.setUp()
+        savedMetaClass = HqlBuilder.metaClass
+        def emc = new ExpandoMetaClass(HqlBuilder, true, true)
+        //emc.log = LogFactory.getLog(getClass())
+        emc.initialize()
+        GroovySystem.metaClassRegistry.setMetaClass(HqlBuilder, emc)
+    }
+
+    protected void tearDown() {
+        GroovySystem.metaClassRegistry.removeMetaClass(HqlBuilder)
+        GroovySystem.metaClassRegistry.setMetaClass(HqlBuilder, savedMetaClass)
+        super.tearDown()
+    }
+
+    void testSelectBasic() {
+
+        def q = new HqlBuilder().query {
+            select 'count(distinct book)'
+            from 'Book as book'
+            where 'book.id > 100'
+        }
+
+        assert q.query == 'select count(distinct book) from Book as book where book.id > 100'
+
+        q.select = 'distinct book'
+        assert q.query == 'select distinct book from Book as book where book.id > 100'
+        assert q.printFormattedQuery == 'select distinct book \nfrom Book as book \nwhere book.id > 100'
+
+    } // testSelectBasic()
+
+    void testSelectAdditional() {
+
+        def q = new HqlBuilder().query {
+            select 'distinct book'
+            from 'Book as book'
+            left 'join book.group as group',
+                    'left join group.type as type'
+            where 'book.id > 100',
+                        'and group = :group'
+        }
+
+        assert q.query == 'select distinct book from Book as book left join book.group as group left join group.type as type where book.id > 100 and group = :group'
+
+    } // testSelectAdditional()
+
+    void testSelectAlternate() {
+
+        def q = new HqlBuilder()
+
+        q {
+            select 'distinct book'
+            from 'Book as book'
+            where(/book.name like '%Ned%'/) // Slashy string literals have to be protected when calling a function.
+            where 'and book.description like "Head"'
+        }
+
+        assert q.query == /select distinct book from Book as book where book.name like '%Ned%' and book.description like "Head"/
+        assert q.printFormattedQuery == /select distinct book ${n}from Book as book ${n}where book.name like '%Ned%' ${n}and book.description like "Head"/
+
+    } // testSelectAlternate()
+
+    void testSelectWithPlaceHolder() {
+
+        def q = new HqlBuilder().query {
+            select 'distinct book'
+            from 'Book as book'
+            where '' // Place holder.
+            order 'by book.name asc'
+        }
+
+        // Insert to place holder which is in the middle of query string.
+        q.where = /book.name like '%Ned%'/  // Slashy string literals don't need protecting when assigning.
+
+        assert q.query == /select distinct book from Book as book where book.name like '%Ned%' order by book.name asc/
+
+    } // testSelectWithPlaceHolder()
+
+    void testSelectWithClauseRemoval() {
+
+        def q = new HqlBuilder().query {
+            select 'count(distinct book)'
+            from 'Book as book'
+            where = /book.name like '%Ned%'/  // Slashy string literals don't need protecting when assigning.
+            order 'by book.name asc'
+        }
+
+        q.order = null // Remove clause, since order by makes no sense when selecting a count ;-)
+        assert q.query == /select count(distinct book) from Book as book where book.name like '%Ned%'/
+
+    } // testSelectWithClauseRemoval()
+
+} // end class
