org.synyx.hades.dao.query.QueryCreator.java Source code

Java tutorial

Introduction

Here is the source code for org.synyx.hades.dao.query.QueryCreator.java

Source

/*
 * Copyright 2008-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.
 */
package org.synyx.hades.dao.query;

import static org.synyx.hades.dao.query.QueryUtils.*;
import static org.synyx.hades.util.ClassUtils.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.synyx.hades.domain.Order;
import org.synyx.hades.domain.Sort.Property;

/**
 * Class to encapsulate query creation logic for {@link QueryMethod}s.
 * 
 * @author Oliver Gierke
 */
class QueryCreator {

    private static final Logger LOG = LoggerFactory.getLogger(QueryCreator.class);
    private static final String INVALID_PARAMETER_SIZE = "You have to provide method arguments for each query "
            + "criteria to construct the query correctly!";

    private static final String AND = "And";
    private static final String OR = "Or";

    private static final String KEYWORD_TEMPLATE = "(%s)(?=[A-Z])";
    private static final String PREFIX_TEMPLATE = "^%s(?=[A-Z]).*";

    private QueryMethod method;

    /**
     * Creates a new {@link QueryCreator} for the given {@link QueryMethod}.
     * 
     * @param finderMethod
     */
    public QueryCreator(QueryMethod finderMethod) {

        Assert.isTrue(!finderMethod.isModifyingQuery());

        this.method = finderMethod;
    }

    /**
     * Constructs a query from the underlying {@link QueryMethod}.
     * 
     * @return the query string
     * @throws QueryCreationException
     */
    String constructQuery() {

        StringBuilder queryBuilder = new StringBuilder(
                getQueryString(READ_ALL_QUERY, getEntityName(method.getDomainClass())));
        queryBuilder.append(" where ");

        PartSource source = new PartSource(method.getName());

        // Split OR
        List<PartSource> orParts = source.getParts(OR);

        int parametersBound = 0;

        for (PartSource orPart : orParts) {

            // Split AND
            List<PartSource> andParts = orPart.getParts(AND);
            StringBuilder andBuilder = new StringBuilder();

            for (PartSource andPart : andParts) {

                Parameters parameters = method.getParameters().getBindableParameters();

                Parameter parameter = parameters.hasParameterAt(parametersBound)
                        ? parameters.getParameter(parametersBound)
                        : null;

                try {
                    Part part = new Part(andPart.cleanedUp(), method, parameter);

                    andBuilder.append(part.getQueryPart()).append(" and ");
                    parametersBound += part.getNumberOfArguments();

                } catch (ParameterOutOfBoundsException e) {
                    throw QueryCreationException.create(method, e);
                }

            }

            andBuilder.delete(andBuilder.length() - 5, andBuilder.length());

            queryBuilder.append(andBuilder);
            queryBuilder.append(" or ");
        }

        // Assert correct number of parameters
        if (!method.isCorrectNumberOfParameters(parametersBound)) {
            throw QueryCreationException.create(method, INVALID_PARAMETER_SIZE);
        }

        queryBuilder.delete(queryBuilder.length() - 4, queryBuilder.length());

        if (source.hasOrderByClause()) {
            queryBuilder.append(" ").append(source.getOrderBySource().getClause());
        }

        String query = queryBuilder.toString();

        LOG.debug("Created query '{}' from method {}", query, method.getName());

        return query;
    }

    /**
     * A single part of a method name that has to be transformed into a query
     * part. The actual transformation is defined by a {@link Type} that is
     * determined from inspecting the given part. The query part can then be
     * looked up via {@link #getQueryPart()}.
     * 
     * @author Oliver Gierke
     */
    private static class Part {

        private final String part;

        private final Type type;
        private final Parameter parameter;
        private final QueryMethod method;

        /**
         * Creates a new {@link Part} from the given method name part, the
         * {@link QueryMethod} the part originates from and the start parameter
         * index.
         * 
         * @param part
         * @param method
         * @param parameter
         */
        public Part(String part, QueryMethod method, Parameter parameter) {

            this.part = part;
            this.type = Type.fromProperty(part, method);
            this.method = method;
            this.parameter = parameter;
        }

        /**
         * Returns how many method parameters are bound by this part.
         * 
         * @return
         */
        public int getNumberOfArguments() {

            return type.getNumberOfArguments();
        }

        /**
         * Returns the query part.
         * 
         * @return
         */
        public String getQueryPart() {

            String property = type.extractProperty(part);

            if (!method.isValidField(property)) {
                throw QueryCreationException.invalidProperty(method, property);
            }

            return type.createQueryPart(StringUtils.uncapitalize(property), parameter);
        }

        /**
         * The type of a method name part. Used to create query parts in various
         * ways.
         * 
         * @author Oliver Gierke
         */
        private static enum Type {

            /**
             * Property to be bound to a {@code between} statement.
             */
            BETWEEN(null, "Between") {

                /**
                 * Binds 2 parameters.
                 */
                @Override
                public int getNumberOfArguments() {

                    return 2;
                }

                /**
                 * Creates part in the form of {@code x.$ property} between ?
                 * and ?} or the named equivalent.
                 */
                @Override
                public String createQueryPart(String property, Parameter parameter) {

                    String first = parameter.getPlaceholder();
                    String second = parameter.getNext().getPlaceholder();

                    return String.format("x.%s between %s and %s", property, first, second);
                }

            },

            IS_NOT_NULL(null, "IsNotNull", "NotNull") {

                @Override
                public int getNumberOfArguments() {

                    return 0;
                }

                @Override
                public String createQueryPart(String property, Parameter parameter) {

                    return String.format("x.%s is not null", property);
                }
            },

            IS_NULL(null, "IsNull", "Null") {

                @Override
                public int getNumberOfArguments() {

                    return 0;
                }

                @Override
                public String createQueryPart(String property, Parameter parameter) {

                    return String.format("x.%s is null", property);
                }
            },

            LESS_THAN("<", "LessThan"), GREATER_THAN(">", "GreaterThan"), NOT_LIKE("not like",
                    "NotLike"), LIKE("like", "Like"), NEGATING_SIMPLE_PROPERTY("<>", "Not"), SIMPLE_PROPERTY("=");

            // Need to list them again explicitly as the order is important
            // (esp. for IS_NULL, IS_NOT_NULL)
            private static final List<Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN,
                    GREATER_THAN, NOT_LIKE, LIKE, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY);
            private List<String> keywords;
            private String operator;

            /**
             * Creates a new {@link Type} using the given keyword and operator.
             * Both can be {@literal null}.
             * 
             * @param operator
             * @param keywords
             */
            private Type(String operator, String... keywords) {

                this.keywords = Arrays.asList(keywords);
                this.operator = operator;
            }

            /**
             * Returns the {@link Type} of the {@link Part} for the given raw
             * property and the given {@link QueryMethod}. This will try to
             * detect e.g. keywords contained in the raw property that trigger
             * special query creation. Returns {@link #SIMPLE_PROPERTY} by
             * default.
             * 
             * @param rawProperty
             * @param method
             * @return
             */
            public static Type fromProperty(String rawProperty, QueryMethod method) {

                for (Type type : ALL) {
                    if (type.supports(rawProperty, method)) {
                        return type;
                    }
                }

                return SIMPLE_PROPERTY;
            }

            /**
             * Create the actual query part for the given property. Creates a
             * simple assignment of the following shape by default. {@code x.$
             * property} ${operator} ${parameterPlaceholder}}.
             * 
             * @param property the actual clean property
             * @param parameters
             * @param index
             * @return
             */
            public String createQueryPart(String property, Parameter parameter) {

                return String.format("x.%s %s %s", property, operator, parameter.getPlaceholder());
            }

            /**
             * Returns whether the the type supports the given raw property.
             * Default implementation checks,. whether the property ends with
             * the registered keyword. Does not support the keyword if the
             * property is a valid field as is.
             * 
             * @param property
             * @param method
             * @return
             */
            protected boolean supports(String property, QueryMethod method) {

                if (keywords == null) {
                    return true;
                }

                if (method.isValidField(property)) {
                    return false;
                }

                for (String keyword : keywords) {
                    if (property.endsWith(keyword)) {
                        return true;
                    }
                }

                return false;
            }

            /**
             * Returns the number of arguments the property binds. By default
             * this exactly one argument.
             * 
             * @return
             */
            public int getNumberOfArguments() {

                return 1;
            }

            /**
             * Callback method to extract the actual property to be bound from
             * the given part. Strips the keyword from the part's end if
             * available.
             * 
             * @param part
             * @return
             */
            public String extractProperty(String part) {

                for (String keyword : keywords) {
                    if (part.endsWith(keyword)) {
                        return part.substring(0, part.indexOf(keyword));
                    }
                }

                return part;
            }
        }
    }

    /**
     * Helper class to split a method name into all of its logical parts
     * (prefix, properties, postfix).
     * 
     * @author Oliver Gierke
     */
    private static class PartSource {

        private static final String ORDER_BY = "OrderBy";
        private static final String[] PREFIXES = new String[] { "findBy", "find", "readBy", "read", "getBy",
                "get" };

        private final String cleanedUpString;
        private final OrderBySource orderBySource;

        public PartSource(String methodName) {

            String removedPrefixes = strip(methodName);

            String[] parts = split(removedPrefixes, ORDER_BY);

            if (parts.length > 2) {
                throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!");
            }

            this.cleanedUpString = parts[0];
            this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1]) : null;
        }

        public OrderBySource getOrderBySource() {

            return orderBySource;
        }

        public boolean hasOrderByClause() {

            return orderBySource != null;
        }

        public List<PartSource> getParts(String keyword) {

            List<PartSource> parts = new ArrayList<PartSource>();
            for (String part : split(cleanedUpString, keyword)) {
                parts.add(new PartSource(part));
            }

            return parts;
        }

        public String cleanedUp() {

            return cleanedUpString;
        }

        /**
         * Strips a prefix from the given method name if it starts with one of
         * {@value #PREFIXES}.
         * 
         * @param methodName
         * @return
         */
        private String strip(String methodName) {

            for (String prefix : PREFIXES) {

                String regex = String.format(PREFIX_TEMPLATE, prefix);
                if (methodName.matches(regex)) {
                    return methodName.substring(prefix.length());
                }
            }

            return methodName;
        }

        /**
         * Splits the given text at the given keywords. Expects camelcase style
         * to only match concrete keywords and not derivatives of it.
         * 
         * @param text
         * @param keyword
         * @return
         */
        private String[] split(String text, String keyword) {

            String regex = String.format(KEYWORD_TEMPLATE, keyword);

            Pattern pattern = Pattern.compile(regex);
            return pattern.split(text);
        }
    }

    /**
     * Simple helper class to create a JPA order by clause from a method name
     * end. It expects the last part of the method name to be given and supports
     * lining up multiple properties ending with the sorting direction. So the
     * following method end would be valid: {@code LastnameUsernameDesc}. This
     * would create a clause {@code order by x.lastname, x.username desc}.
     * 
     * @author Oliver Gierke
     */
    static class OrderBySource {

        private final String BLOCK_SPLIT = "(?<=Asc|Desc)(?=[A-Z])";
        private final Pattern DIRECTION_SPLIT = Pattern.compile("(.+)(Asc|Desc)$");

        private final List<Property> orders;

        public OrderBySource(String clause) {

            this.orders = new ArrayList<Property>();

            for (String part : clause.split(BLOCK_SPLIT)) {

                Matcher matcher = DIRECTION_SPLIT.matcher(part);

                if (!matcher.find()) {
                    throw new IllegalArgumentException(String.format("Invalid order syntax for part %s!", part));
                }

                Order direction = Order.fromJpaValue(matcher.group(2));
                String property = StringUtils.uncapitalize(matcher.group(1));

                this.orders.add(new Property(direction, property));
            }
        }

        /**
         * Returns the final JPA order by clause.
         * 
         * @return
         */
        public String getClause() {

            StringBuilder builder = new StringBuilder("order by ");

            for (Property property : this.orders) {

                builder.append("x.").append(property.getName());
                builder.append(" ").append(property.getOrder().getJpaValue());
                builder.append(", ");
            }

            builder.delete(builder.length() - 2, builder.length());
            return builder.toString();
        }
    }
}