Package grails.plugin.searchable.internal.compass.search

Source Code of grails.plugin.searchable.internal.compass.search.GroovyCompassQueryBuilder$CompassQueryBuildingClosureDelegate

/*
* Copyright 2007 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.
*/
package grails.plugin.searchable.internal.compass.search;

import groovy.lang.Closure;
import groovy.lang.GroovyObjectSupport;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.queryParser.QueryParser;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.compass.core.CompassQuery;
import org.compass.core.CompassQueryBuilder;
import org.compass.core.util.Assert;
import org.compass.core.util.ClassUtils;

/**
* A Groovy CompassQuery builder, taking nested closures and dynamic method
* invocation in the Groovy-builder style
*
* Note: Instances of this class are NOT thread-safe: you should create one
* for the duration of your CompassSession then discard it
*
* @author Maurice Nicholson
*/
public class GroovyCompassQueryBuilder extends GroovyObjectSupport {
    private CompassQueryBuilder queryBuilder;

    /**
     * Constructor
     * @param queryBuilder a CompassQueryBuilder used internally to build the query
     */
    public GroovyCompassQueryBuilder(CompassQueryBuilder queryBuilder) {
        this.queryBuilder = queryBuilder;
    }

    /**
     * Build a CompassQuery for the given closure and return it
     * @param closure a Closure which builds the query when executed
     * @return a CompassQuery built by the given Closure
     */
    public CompassQuery buildQuery(Closure closure) {
        CompassQueryBuildingClosureDelegate invoker = new CompassQueryBuildingClosureDelegate(queryBuilder);
        closure = (Closure) closure.clone();
        closure.setDelegate(invoker);
        Object result;
        if (closure.getMaximumNumberOfParameters() == 1) {
            result = closure.call(queryBuilder);
        } else {
            result = closure.call();
        }
        CompassQuery query = invoker.getQuery();
        return (CompassQuery) ((query == null) ? result : query);
    }

    /**
     * Directly invoke a builder method
     */
    @Override
    public Object invokeMethod(String name, Object args) {
        CompassQueryBuildingClosureDelegate invoker = new CompassQueryBuildingClosureDelegate(queryBuilder);
        InvokerHelper.invokeMethod(invoker, name, args);
        return invoker.getQuery();
    }

    /**
     * This class acts as the query-builder closure delegate, identifying
     * which method to call on which object and tying the result together at
     * the end
     *
     * Note: Instances of this class are NOT thread-safe
     */
    private static class CompassQueryBuildingClosureDelegate extends GroovyObjectSupport {
        private static final Log LOG = LogFactory.getLog(CompassQueryBuildingClosureDelegate.class);
        private static final Map SHORT_METHOD_NAMES = shortMethodNames();
        private static Map shortMethodNames() {
            Map shortMethodNames = new HashMap();
            shortMethodNames.put("should", "addShould");
            shortMethodNames.put("must", "addMust");
            shortMethodNames.put("mustNot", "addMustNot");
            shortMethodNames.put("sort", "addSort");
            return shortMethodNames;
        }
        private static final List BOOLEAN_ADDER_NAMES = booleanAdderNames();
        private static List booleanAdderNames() {
            List booleanAdderNames = new ArrayList();
            booleanAdderNames.add("addShould");
            booleanAdderNames.add("addMust");
            booleanAdderNames.add("addMustNot");
            return booleanAdderNames;
        }
        private static final Map SHORT_OPTION_NAMES = shortOptionNames();
        private static Map shortOptionNames() {
            Map shortOptionNames = new HashMap();
            shortOptionNames.put("defaultProperty", "defaultSearchProperty");
            shortOptionNames.put("andDefaultOperator", "useAndDefaultOperator");
            shortOptionNames.put("parser", "queryParser");
            return shortOptionNames;
        }

        private CompassQueryBuilder queryBuilder;
        private Stack stack = new Stack();
        private int depth = 0;
        private Object previous; // previous method invocation result, if any

        public CompassQueryBuildingClosureDelegate(CompassQueryBuilder queryBuilder) {
            this.queryBuilder = queryBuilder;
        }

        @Override
        public Object invokeMethod(String name, Object args) {
            if (isTraceEnabled()) {
                trace("invokeMethod(" + name + ", " + args + ")");
            }
            depth++;

            // Remove Closure and options Map
            Assert.isInstanceOf(Object[].class, args);
            List invokeArgs = new ArrayList();
            invokeArgs.addAll(Arrays.asList(((Object[]) args)));
            Closure closure = (Closure) remove(invokeArgs, Closure.class);
            Map options = (Map) remove(invokeArgs, Map.class);

            // Escape String queries?
            if (name.equals("queryString") && options != null && MapUtils.getBoolean(options, "escape")) {
                Assert.isInstanceOf(String.class, invokeArgs.get(0));
                invokeArgs.set(0, QueryParser.escape((String) invokeArgs.get(0)));
                options.remove("escape");
            }

            // Transform method name?
            if (SHORT_METHOD_NAMES.containsKey(name)) {
                name = (String) SHORT_METHOD_NAMES.get(name);
            }

            // Method to call is *on* CompassQuery but we have builder?
            boolean queryMethod = hasMethod(CompassQuery.class, name) && !hasMethod(queryBuilder, name);
            if (queryMethod && !hasMethod(peek(), name) && hasToQuery(peek())) {
                maybeAddPreviousShould(); // Lazy boolean?
                Object temp = pop();
                if (isTraceEnabled()) {
                    trace("converting " + getShortClassName(temp) + " to query for " + name);
                }
                temp = toQuery(temp);
                push(temp);
            } else if (queryMethod && !hasMethod(previous, name) && hasToQuery(previous)) {
                if (isTraceEnabled()) {
                    trace("converting " + getShortClassName(previous) + " to query for " + name);
                }
                previous = toQuery(previous);
            }

            // Implicit boolean?
            if (BOOLEAN_ADDER_NAMES.contains(name) && !isWithinBool()) {
                trace("Implicit boolean -- " + name + " called when the stack is " + stack + ", args " + invokeArgs);
                Object bool = queryBuilder.bool();
                push(bool);
            }

            // Method to call takes CompassQuery but we have Builder?
            if (BOOLEAN_ADDER_NAMES.contains(name)) { // && !isQuery(invokeArgs[0])) {
                if (invokeArgs.isEmpty()) {
                    Assert.notNull(closure, "Attempt to call " + name + " without a query or closure argument");
                    maybeAddPreviousShould();
                    trace("executing nested boolean closure");
                    Object temp = previous;
                    previous = null;
                    Object innerBool = InvokerHelper.invokeMethod(this, "bool", closure);
                    invokeArgs = new ArrayList();
                    invokeArgs.add(innerBool);
                    closure = null;
                    previous = temp;
                    trace("done nested boolean closure");
                }
                if (invokeArgs.size() > 0 && hasToQuery(invokeArgs.get(0))) {
                    CompassQuery query = toQuery(invokeArgs.get(0));
                    invokeArgs = new ArrayList();
                    invokeArgs.add(query);
                }
            }

            // Convert any BigDecimals to floats: simple but does the job for now
            for (int i = 0; i < invokeArgs.size(); i++) {
                if (invokeArgs.get(i) instanceof BigDecimal) {
                    invokeArgs.set(i, new Float(((BigDecimal) invokeArgs.get(i)).floatValue()));
                }
            }

            Object result = null;
            if (hasMethod(peek(), name)) {
                if (isTraceEnabled()) {
                    trace("invoking " + peek().getClass().getName() + "." + name + "(" + invokeArgs+ ")");
                }
                result = InvokerHelper.invokeMethod(pop(), name, invokeArgs.toArray());
                if (isTraceEnabled()) {
                    trace("result is " + result.getClass().getName() + " " + result);
                }
                push(result);
            } else if (hasMethod(queryBuilder, name)) {
                // Eager implicit boolean check for previous should clause
                if (previous != null && !isWithinBool()) {
                    trace("implicit boolean spotted");
                    CompassQueryBuilder.CompassBooleanQueryBuilder bool = queryBuilder.bool();
                    bool.addShould(toQuery(previous));
                    push(bool);
                    previous = null; // not necessary?
                } else {
                    // Lazy boolean?
                    maybeAddPreviousShould();
                }
                if (isTraceEnabled()) {
                    trace("invoking queryBuilder." + name + "(" + invokeArgs + ")");
                }
                result = InvokerHelper.invokeMethod(queryBuilder, name, invokeArgs.toArray());
                if (isTraceEnabled()) {
                    trace("result is " + result.getClass().getName() + " " + result);
                }
            } else if (hasMethod(previous, name)) {
                if (isTraceEnabled()) {
                    trace("invoking " + getShortClassName(previous) + "." + name + "(" + invokeArgs+ ")");
                }
                result = InvokerHelper.invokeMethod(previous, name, invokeArgs.toArray());
                if (isTraceEnabled()) {
                    trace("result is " + result.getClass().getName() + " " + result);
                }
            } else {
                throw new UnsupportedOperationException(
                    "No such method CompassQueryBuilder#" + name +
                    (peek() == null ? "" : " or " + getShortClassName(peek()) + "#" + name) +
                    (previous == null ? "" : " or " + getShortClassName(previous) + "#" + name) +
                    ". (Arguments were " + invokeArgs + ", stack is " + stack + ", result is " + result + ")" + //, this.result is ${this.result}) " +
                    ". Refer to the Compass API docs at http://www.opensymphony.com/compass/versions/1.1/api/org/compass/core/CompassQueryBuilder.html to see what methods are available"
                );
            }

            // Recurse into nested closure?
            if (closure != null) {
                trace("invoking the closure arg");
                push(result);
                Object temp = previous;
                previous = null;
                closure = (Closure) closure.clone();
                closure.setDelegate(this);
                depth++;
                closure.call();
                depth--;

                // Complete semi-implicit boolean?
                maybeAddPreviousShould();

                previous = temp;
                result = pop();
            }

            // Apply builder/query options?
            result = applyOptions(result, options);
            previous = result;
            depth--;

            if (isTraceEnabled()) {
                trace("after methods and closure, depth " + depth + ", stack " + stack + ", previous " + previous);
                trace("returning " + result);
            }
            return result;
        }

        private void maybeAddPreviousShould() {
            if (previous != null && isWithinBool() && !previous.equals(peek())) {
                trace("previous lazy boolean should clause spotted");
                InvokerHelper.invokeMethod(peek(), "addShould", toQuery(previous));
                previous = null; // not necessary?
            }
        }

        private Object applyOptions(Object result, Map options) {
            if (options == null || options.size() == 0) {
                return result;
            }

            // Convert shorthand to longhand names
            Map tmp = new HashMap();
            for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iter.next();
                String optionName = (String) entry.getKey();
                if (SHORT_OPTION_NAMES.containsKey(optionName)) {
                    tmp.put(SHORT_OPTION_NAMES.get(optionName), entry.getValue());
                } else {
                    tmp.put(optionName, entry.getValue());
                }
            }
            options = tmp;

            // Convert any BigDecimals to floats: simple but does the job for now
            for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iter.next();
                String optionName = (String) entry.getKey();
                Object value = entry.getValue();
                if (value instanceof BigDecimal) {
                    options.put(optionName, new Float(((BigDecimal) value).floatValue()));
                }
            }

            if (isTraceEnabled()) {
                trace("applying options " + options + " to " + result);
            }
            Map queryOptions = new HashMap();

            boolean hasToQuery = !isQuery(result) && hasToQuery(result);
            for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iter.next();
                String optionName = (String) entry.getKey();
                Object optionValue = entry.getValue();
                // special case for this single no-parameter, non-setter string query builder method
                if (optionName.equals("useAndDefaultOperator")) {
                    if (optionValue != null) {
                        Assert.isTrue(result instanceof CompassQueryBuilder.CompassMultiPropertyQueryStringBuilder || result instanceof  CompassQueryBuilder.CompassQueryStringBuilder, "'useAndDefaultOperator' option provided when current query/builder is a " + getShortClassName(result) + ", but should be a Compass*QueryStringBuilder");
                        if (optionValue.equals(Boolean.TRUE)) {
                            InvokerHelper.invokeMethod(result, "useAndDefaultOperator", null);
                        } else {
                            InvokerHelper.invokeMethod(result, "useOrDefaultOperator", null);
                        }
                    }
                    continue;
                }
                if (optionName.equals("defaultOperator")) {
                    Assert.notNull(optionValue, "'defaultOperator' option value is null: it must be one of 'or' or 'and'");
                    Assert.isInstanceOf(String.class, optionValue, "'defaultOperator' option value is must be a String but is: [" + optionValue.getClass().getName() + "]");
                    if (((String)optionValue).equalsIgnoreCase("or")) {
                        InvokerHelper.invokeMethod(result, "useOrDefaultOperator", null);
                    } else if (((String)optionValue).equalsIgnoreCase("and")) {
                        InvokerHelper.invokeMethod(result, "useAndDefaultOperator", null);
                    } else {
                        throw new IllegalArgumentException("'defaultOperator' option value is not valid: it must be one of 'or' or 'and' but was '" + optionValue + "'");
                    }
                    continue;
                }
                String methodName = "set" + optionName.substring(0, 1).toUpperCase() + optionName.substring(1);
                trace("method is -- " + methodName + "(" + optionValue + ")");
                if (hasMethod(result, methodName)) {
                    if (isTraceEnabled()) {
                        trace("invoking " + getShortClassName(result) + "." + methodName + "(" + optionValue + ")");
                    }
                    result = InvokerHelper.invokeMethod(result, methodName, optionValue);
                } else if (hasToQuery && hasMethod(CompassQuery.class, methodName)) {
                    trace("added option to queryOptions, " + queryOptions);
                    queryOptions.put(optionName, optionValue);
                } else {
                    throw new UnsupportedOperationException(
                        "No such method " + getShortClassName(result) + "#" + methodName + (!hasToQuery ? "" : " or CompassQuery#" + methodName) +
                        ". (Arguments were " + optionValue + ") " +
                        ". Refer to the Compass API docs at http://www.opensymphony.com/compass/versions/1.1/api/org/compass/core/CompassQueryBuilder.html to see what methods are available"
                    );
                }
            }

            if (!queryOptions.isEmpty()) {
                trace("setting query options");
                result = toQuery(result);
                for (Iterator iter = queryOptions.entrySet().iterator(); iter.hasNext(); ) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    String optionName = (String) entry.getKey();
                    Object optionValue = entry.getValue();
                    String methodName = "set" + optionName.substring(0, 1).toUpperCase() + optionName.substring(1);
                    if (isTraceEnabled()) {
                        trace("method is -- " + methodName + "(" + optionValue + ")");
                    }
                    Assert.isTrue(hasMethod(result, methodName));
                    if (isTraceEnabled()) {
                        trace("invoking " + getShortClassName(result) + "#" + methodName + "(" + optionValue + ")");
                    }
                    result = InvokerHelper.invokeMethod(result, methodName, optionValue);
                }
            }

            return result;
        }

        public CompassQuery getQuery() {
            if (previous != null) {
                boolean completeBoolean = false;
                if (isWithinBool() && !previous.equals(peek())) {
                    if (isTraceEnabled()) {
                        trace("Within bool " + peek() + ", and previous is " + previous);
                    }
                    completeBoolean = true;
                }
                if (!isQuery(previous)) {
                    trace("converting result to query for caller");
                    previous = toQuery(previous);
                }
                if (completeBoolean) {
                    trace("completing boolean");
                    InvokerHelper.invokeMethod(peek(), "addShould", previous);
                    previous = toQuery(pop());
                }
            }
            return (CompassQuery) previous;
        }

        private boolean isWithinBool() {
            return isBoolBuilder(peek());
        }

        private void push(Object object) {
            stack.push(object);
            if (isTraceEnabled()) {
                trace("pushed " + getShortClassName(object) + " " + object + " to stack");
            }
        }

        private Object pop() {
            if (isTraceEnabled()) {
                trace("popping " + (peek() == null ? "null" : (getShortClassName(peek()) + " " + peek())) + " from the stack");
            }
            return stack.empty() ? null : stack.pop();
        }

        private Object peek() {
            return stack.empty() ? null : stack.peek();
        }

        private boolean hasMethod(Object thing, String name) {
            if (thing == null) {
                return false;
            }
            Class clazz = (Class) (thing instanceof Class ? thing : thing.getClass());
            Method[] methods = clazz.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                if (method.getName().equals(name)) {
                    return true;
                }
            }
            return false;
        }

        private boolean hasToQuery(Object object) {
            return object instanceof CompassQueryBuilder.ToCompassQuery;
        }

        private CompassQuery toQuery(Object object) {
            if (isQuery(object)) return (CompassQuery) object;
            return ((CompassQueryBuilder.ToCompassQuery) object).toQuery();
        }

        private boolean isQuery(Object object) {
            return object instanceof CompassQuery;
        }

        private boolean isBoolBuilder(Object object) {
            return object instanceof CompassQueryBuilder.CompassBooleanQueryBuilder;
        }

        private String getShortClassName(Object thing) {
            if (thing == null) {
                return "null";
            }
            Class clazz = (Class) (thing instanceof Class ? thing : thing.getClass());
            return ClassUtils.getShortName(clazz);
        }

        private Object remove(List args, Class clazz) {
            if (args == null) {
                return null;
            }
            for (int i = 0; i < args.size(); i++) {
                if (clazz.isAssignableFrom(args.get(i).getClass())) {
                    return args.remove(i);
                }
            }
            return null;
        }

        private boolean isTraceEnabled() {
            return LOG.isTraceEnabled();
        }

        private void trace(String message) {
            StringBuilder buf = new StringBuilder(message.length() + depth * 2);
            for (int i = 0; i < depth; i++) {
                buf.append("  ");
            }
            buf.append(message);
            LOG.trace(buf);
        }
    }
}
TOP

Related Classes of grails.plugin.searchable.internal.compass.search.GroovyCompassQueryBuilder$CompassQueryBuildingClosureDelegate

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.