org.grails.datastore.gorm.finders.DynamicFinder.java Source code

Java tutorial

Introduction

Here is the source code for org.grails.datastore.gorm.finders.DynamicFinder.java

Source

/* Copyright (C) 2010 SpringSource
 *
 * 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 org.grails.datastore.gorm.finders;

import grails.gorm.DetachedCriteria;
import groovy.lang.Closure;
import groovy.lang.MissingMethodException;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.grails.datastore.gorm.finders.MethodExpression.Between;
import org.grails.datastore.gorm.finders.MethodExpression.Equal;
import org.grails.datastore.gorm.finders.MethodExpression.GreaterThan;
import org.grails.datastore.gorm.finders.MethodExpression.GreaterThanEquals;
import org.grails.datastore.gorm.finders.MethodExpression.Ilike;
import org.grails.datastore.gorm.finders.MethodExpression.InList;
import org.grails.datastore.gorm.finders.MethodExpression.InRange;
import org.grails.datastore.gorm.finders.MethodExpression.IsEmpty;
import org.grails.datastore.gorm.finders.MethodExpression.IsNotEmpty;
import org.grails.datastore.gorm.finders.MethodExpression.IsNotNull;
import org.grails.datastore.gorm.finders.MethodExpression.IsNull;
import org.grails.datastore.gorm.finders.MethodExpression.LessThan;
import org.grails.datastore.gorm.finders.MethodExpression.LessThanEquals;
import org.grails.datastore.gorm.finders.MethodExpression.Like;
import org.grails.datastore.gorm.finders.MethodExpression.NotEqual;
import org.grails.datastore.gorm.finders.MethodExpression.Rlike;
import org.grails.datastore.mapping.core.Datastore;
import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.model.types.Basic;
import org.grails.datastore.mapping.query.Query;
import org.grails.datastore.mapping.query.api.QueryArgumentsAware;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService;
import org.springframework.util.StringUtils;

/**
 * Abstract base class for dynamic finders.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class DynamicFinder extends AbstractFinder implements QueryBuildingFinder {

    public static final String ARGUMENT_MAX = "max";
    public static final String ARGUMENT_OFFSET = "offset";
    public static final String ARGUMENT_ORDER = "order";
    public static final String ARGUMENT_SORT = "sort";
    public static final String ORDER_DESC = "desc";
    public static final String ORDER_ASC = "asc";
    public static final String ARGUMENT_FETCH = "fetch";
    public static final String ARGUMENT_IGNORE_CASE = "ignoreCase";
    public static final String ARGUMENT_CACHE = "cache";
    public static final String ARGUMENT_LOCK = "lock";

    protected Pattern pattern;
    private Pattern[] operatorPatterns;
    private String[] operators;

    private static Pattern methodExpressinPattern;
    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];

    private static final String NOT = "Not";
    private static final Map<String, Constructor> methodExpressions = new LinkedHashMap<String, Constructor>();

    static {
        // populate the default method expressions
        try {
            Class[] classes = { Equal.class, NotEqual.class, InList.class, InRange.class, Between.class, Like.class,
                    Ilike.class, Rlike.class, GreaterThanEquals.class, LessThanEquals.class, GreaterThan.class,
                    LessThan.class, IsNull.class, IsNotNull.class, IsEmpty.class, IsEmpty.class, IsNotEmpty.class };
            Class[] constructorParamTypes = { Class.class, String.class };
            for (Class c : classes) {
                methodExpressions.put(c.getSimpleName(), c.getConstructor(constructorParamTypes));
            }
        } catch (SecurityException e) {
            // ignore
        } catch (NoSuchMethodException e) {
            // ignore
        }

        resetMethodExpressionPattern();
    }

    static void resetMethodExpressionPattern() {
        String expressionPattern = DefaultGroovyMethods.join(methodExpressions.keySet(), "|");
        methodExpressinPattern = Pattern.compile("\\p{Upper}[\\p{Lower}\\d]+(" + expressionPattern + ")");
    }

    protected DynamicFinder(final Pattern pattern, final String[] operators, final Datastore datastore) {
        super(datastore);
        this.pattern = pattern;
        this.operators = operators;
        this.operatorPatterns = new Pattern[operators.length];
        for (int i = 0; i < operators.length; i++) {
            operatorPatterns[i] = Pattern.compile("(\\w+)(" + operators[i] + ")(\\p{Upper})(\\w+)");
        }
    }

    /**
     * Registers a new method expression. The Class must extends from the class {@link MethodExpression} and provide
     * a constructor that accepts a Class parameter and a String parameter.
     *
     * @param methodExpression A class that extends from {@link MethodExpression}
     */
    public static void registerNewMethodExpression(Class methodExpression) {
        try {
            methodExpressions.put(methodExpression.getSimpleName(),
                    methodExpression.getConstructor(Class.class, String.class));
            resetMethodExpressionPattern();
        } catch (SecurityException e) {
            throw new IllegalArgumentException("Class [" + methodExpression
                    + "] does not provide a constructor that takes parameters of type Class and String: "
                    + e.getMessage(), e);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Class [" + methodExpression
                    + "] does not provide a constructor that takes parameters of type Class and String: "
                    + e.getMessage(), e);
        }
    }

    public void setPattern(String pattern) {
        this.pattern = Pattern.compile(pattern);
    }

    public boolean isMethodMatch(String methodName) {
        return pattern.matcher(methodName.subSequence(0, methodName.length())).find();
    }

    public Object invoke(final Class clazz, String methodName, Closure additionalCriteria, Object[] arguments) {
        DynamicFinderInvocation invocation = createFinderInvocation(clazz, methodName, additionalCriteria,
                arguments);
        return doInvokeInternal(invocation);
    }

    public Object invoke(final Class clazz, String methodName, DetachedCriteria detachedCriteria,
            Object[] arguments) {
        DynamicFinderInvocation invocation = createFinderInvocation(clazz, methodName, null, arguments);
        if (detachedCriteria != null) {
            invocation.setDetachedCriteria(detachedCriteria);
        }
        return doInvokeInternal(invocation);
    }

    public DynamicFinderInvocation createFinderInvocation(Class clazz, String methodName,
            Closure additionalCriteria, Object[] arguments) {

        List expressions = new ArrayList();
        if (arguments == null)
            arguments = EMPTY_OBJECT_ARRAY;
        else {
            Object[] tmp = new Object[arguments.length];
            System.arraycopy(arguments, 0, tmp, 0, arguments.length);
            arguments = tmp;
        }
        Matcher match = pattern.matcher(methodName);
        // find match
        match.find();

        String[] queryParameters;
        int totalRequiredArguments = 0;
        // get the sequence clauses
        final String querySequence;
        int groupCount = match.groupCount();
        if (groupCount == 6) {
            String booleanProperty = match.group(3);
            if (booleanProperty == null) {
                booleanProperty = match.group(6);
                querySequence = null;
            } else {
                querySequence = match.group(5);
            }
            Boolean arg = Boolean.TRUE;
            if (booleanProperty.matches("Not[A-Z].*")) {
                booleanProperty = booleanProperty.substring(3);
                arg = Boolean.FALSE;
            }
            MethodExpression booleanExpression = findMethodExpression(clazz, booleanProperty);
            booleanExpression.setArguments(new Object[] { arg });
            expressions.add(booleanExpression);
        } else {
            querySequence = match.group(2);
        }
        // if it contains operator and split
        boolean containsOperator = false;
        String operatorInUse = null;
        if (querySequence != null) {
            for (int i = 0; i < operators.length; i++) {
                Matcher currentMatcher = operatorPatterns[i].matcher(querySequence);
                if (currentMatcher.find()) {
                    containsOperator = true;
                    operatorInUse = operators[i];

                    queryParameters = querySequence.split(operatorInUse);

                    // loop through query parameters and create expressions
                    // calculating the number of arguments required for the expression
                    int argumentCursor = 0;
                    for (String queryParameter : queryParameters) {
                        MethodExpression currentExpression = findMethodExpression(clazz, queryParameter);
                        final int requiredArgs = currentExpression.getArgumentsRequired();
                        // populate the arguments into the GrailsExpression from the argument list
                        Object[] currentArguments = new Object[requiredArgs];
                        if ((argumentCursor + requiredArgs) > arguments.length) {
                            throw new MissingMethodException(methodName, clazz, arguments);
                        }

                        for (int k = 0; k < requiredArgs; k++, argumentCursor++) {
                            currentArguments[k] = arguments[argumentCursor];
                        }
                        currentExpression = getInitializedExpression(currentExpression, currentArguments);
                        PersistentEntity persistentEntity = datastore.getMappingContext()
                                .getPersistentEntity(clazz.getName());

                        try {
                            currentExpression.convertArguments(persistentEntity);
                        } catch (ConversionException e) {
                            throw new MissingMethodException(methodName, clazz, arguments);
                        }

                        // add to list of expressions
                        totalRequiredArguments += currentExpression.argumentsRequired;
                        expressions.add(currentExpression);
                    }
                    break;
                }
            }
        }
        // otherwise there is only one expression
        if (!containsOperator && querySequence != null) {
            MethodExpression solo = findMethodExpression(clazz, querySequence);

            final int requiredArguments = solo.getArgumentsRequired();
            if (requiredArguments > arguments.length) {
                throw new MissingMethodException(methodName, clazz, arguments);
            }

            totalRequiredArguments += requiredArguments;
            Object[] soloArgs = new Object[requiredArguments];
            System.arraycopy(arguments, 0, soloArgs, 0, requiredArguments);
            solo = getInitializedExpression(solo, arguments);
            PersistentEntity persistentEntity = datastore.getMappingContext().getPersistentEntity(clazz.getName());
            try {
                solo.convertArguments(persistentEntity);
            } catch (ConversionException e) {
                if (!(persistentEntity.getPropertyByName(solo.propertyName) instanceof Basic)) {
                    throw new MissingMethodException(methodName, clazz, arguments);
                }
            }
            expressions.add(solo);
        }

        // if the total of all the arguments necessary does not equal the number of arguments
        // throw exception
        if (totalRequiredArguments > arguments.length) {
            throw new MissingMethodException(methodName, clazz, arguments);
        }

        // calculate the remaining arguments
        Object[] remainingArguments = new Object[arguments.length - totalRequiredArguments];
        if (remainingArguments.length > 0) {
            for (int i = 0, j = totalRequiredArguments; i < remainingArguments.length; i++, j++) {
                remainingArguments[i] = arguments[j];
            }
        }

        return new DynamicFinderInvocation(clazz, methodName, remainingArguments, expressions, additionalCriteria,
                operatorInUse);
    }

    /**
     * Initializes the arguments of the specified expression with the specified arguments.  If the
     * expression is an Equal expression and the argument is null then a new expression is created
     * and returned of type IsNull.
     * 
     * @param expression expression to initialize
     * @param arguments arguments to the expression
     * @return the initialized expression
     */
    private MethodExpression getInitializedExpression(MethodExpression expression, Object[] arguments) {
        if (expression instanceof Equal && arguments.length == 1 && arguments[0] == null) {
            expression = new IsNull(expression.targetClass, expression.propertyName);
        } else {
            expression.setArguments(arguments);

        }
        return expression;
    }

    protected MethodExpression findMethodExpression(Class clazz, String expression) {
        MethodExpression me = null;
        final Matcher matcher = methodExpressinPattern.matcher(expression);

        if (matcher.find()) {
            Constructor constructor = methodExpressions.get(matcher.group(1));
            try {
                me = (MethodExpression) constructor.newInstance(clazz,
                        calcPropertyName(expression, constructor.getDeclaringClass().getSimpleName()));
            } catch (Exception e) {
                // ignore
            }
        }
        if (me == null) {
            me = new Equal(clazz, calcPropertyName(expression, Equal.class.getSimpleName()));
        }

        return me;
    }

    private static String calcPropertyName(String queryParameter, String clause) {
        String propName;
        if (clause != null && !clause.equals(Equal.class.getSimpleName())) {
            int i = queryParameter.indexOf(clause);
            propName = queryParameter.substring(0, i);
        } else {
            propName = queryParameter;
        }

        if (propName.endsWith(NOT)) {
            int i = propName.lastIndexOf(NOT);
            propName = propName.substring(0, i);
        }

        if (!StringUtils.hasLength(propName)) {
            throw new IllegalArgumentException("No property name specified in clause: " + clause);
        }

        return propName.substring(0, 1).toLowerCase(Locale.ENGLISH) + propName.substring(1);
    }

    protected abstract Object doInvokeInternal(DynamicFinderInvocation invocation);

    public Object invoke(final Class clazz, String methodName, Object[] arguments) {
        return invoke(clazz, methodName, (Closure) null, arguments);
    }

    public static void populateArgumentsForCriteria(@SuppressWarnings("unused") Class<?> targetClass, Query q,
            Map argMap) {
        if (argMap == null) {
            return;
        }

        Integer maxParam = null;
        Integer offsetParam = null;
        final ConversionService conversionService = q.getSession().getMappingContext().getConversionService();
        if (argMap.containsKey(ARGUMENT_MAX)) {
            maxParam = conversionService.convert(argMap.get(ARGUMENT_MAX), Integer.class);
        }
        if (argMap.containsKey(ARGUMENT_OFFSET)) {
            offsetParam = conversionService.convert(argMap.get(ARGUMENT_OFFSET), Integer.class);
        }
        String orderParam = (String) argMap.get(ARGUMENT_ORDER);

        final String sort = (String) argMap.get(ARGUMENT_SORT);
        final String order = ORDER_DESC.equalsIgnoreCase(orderParam) ? ORDER_DESC : ORDER_ASC;
        final int max = maxParam == null ? -1 : maxParam;
        final int offset = offsetParam == null ? -1 : offsetParam;
        if (max > -1) {
            q.max(max);
        }
        if (offset > -1) {
            q.offset(offset);
        }
        if (sort != null) {
            if (ORDER_DESC.equals(order)) {
                q.order(Query.Order.desc(sort));
            } else {
                q.order(Query.Order.asc(sort));
            }
        }
        if (q instanceof QueryArgumentsAware) {
            ((QueryArgumentsAware) q).setArguments(argMap);
        }
    }

    protected void configureQueryWithArguments(Class clazz, Query query, Object[] arguments) {
        if (arguments.length == 0 || !(arguments[0] instanceof Map)) {
            return;
        }

        Map<?, ?> argMap = (Map<?, ?>) arguments[0];
        populateArgumentsForCriteria(clazz, query, argMap);
    }

    public static void applyDetachedCriteria(Query q, DetachedCriteria detachedCriteria) {
        if (detachedCriteria != null) {
            List<Query.Criterion> criteria = detachedCriteria.getCriteria();
            for (Query.Criterion criterion : criteria) {
                q.add(criterion);
            }
            List<Query.Projection> projections = detachedCriteria.getProjections();
            for (Query.Projection projection : projections) {
                q.projections().add(projection);
            }
            List<Query.Order> orders = detachedCriteria.getOrders();
            for (Query.Order order : orders) {
                q.order(order);
            }
        }
    }

}