Java tutorial
/* * DynaQueryBuilder.java created on 2011-01-30 * * Created by Brushing Bits Labs * http://www.brushingbits.org * * 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.jnap.core.persistence.hibernate; import static java.text.MessageFormat.format; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.hibernate.Criteria; import org.hibernate.QueryException; import org.hibernate.Session; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Junction; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.metadata.ClassMetadata; import org.springframework.util.Assert; /** * TODO refactor to JPA package (remove Hibernate dependency here) * * @author Daniel Rochetti * @since 1.0 */ public class DynaQueryBuilder { private static final String BETWEEN = "Between"; private static final String EQUAL = "Equal"; private static final String GREATER_THAN = "GreaterThan"; private static final String GREATER_THAN_OR_EQUAL = "GreaterThanEqual"; private static final String IS_EMPTY = "IsEmpty"; private static final String IS_NOT_EMPTY = "IsNotEmpty"; private static final String IS_NOT_NULL = "IsNotNull"; private static final String IS_NULL = "IsNull"; private static final String LESS_THAN = "LessThan"; private static final String LESS_THAN_OR_EQUAL = "LessThanEqual"; private static final String LIKE = "Like"; private static final String LIKE_IC = "LikeIc"; private static final String NOT_EQUAL = "NotEqual"; private static final String[] EXPRESSION_OPERATORS = new String[] { BETWEEN, GREATER_THAN_OR_EQUAL, GREATER_THAN, IS_EMPTY, IS_NOT_EMPTY, IS_NOT_NULL, IS_NULL, LESS_THAN_OR_EQUAL, LESS_THAN, LIKE_IC, LIKE, NOT_EQUAL, EQUAL }; private static final String LOGICAL_OPERATOR_OR = "Or"; private static final String LOGICAL_OPERATOR_AND = "And"; private static Pattern DYNA_QUERY_OPERATOR_PATTERN = Pattern .compile(format("({0}|{1})", LOGICAL_OPERATOR_OR, LOGICAL_OPERATOR_AND)); public static Pattern DYNA_QUERY_PATTERN = Pattern.compile("^(findBy|countBy|findUniqueBy)([A-Z]\\w*)"); private static Pattern ORDER_BY_PATTERN = Pattern.compile("((OrderBy)([A-Z]\\w*)(Asc|Desc))$"); private Session session; private String entityName; private String dynaQuery; private Object[] queryParams; private int propertyValueIndex = 0; private ClassMetadata entityMetadata; private Map<String, DynaQueryBuilder.CriterionBuilder> criterionBuilderMap; /** * * @param session * @param entityName * @param dynaQuery * @param queryParams */ public DynaQueryBuilder(Session session, String entityName, String dynaQuery, Object... queryParams) { Assert.notNull(session); Assert.hasText(entityName); Assert.hasText(dynaQuery); this.session = session; this.entityName = entityName; this.dynaQuery = dynaQuery; this.queryParams = queryParams == null ? ArrayUtils.EMPTY_OBJECT_ARRAY : queryParams; this.entityMetadata = this.session.getSessionFactory().getClassMetadata(this.entityName); buildCriterionBuilderStrategy(); } /** * */ private void buildCriterionBuilderStrategy() { this.criterionBuilderMap = new HashMap<String, DynaQueryBuilder.CriterionBuilder>(); this.criterionBuilderMap.put(BETWEEN, new BetweenCriterionBuilder()); this.criterionBuilderMap.put(EQUAL, new EqualCriterionBuilder()); this.criterionBuilderMap.put(GREATER_THAN, new GreaterThanCriterionBuilder()); this.criterionBuilderMap.put(GREATER_THAN_OR_EQUAL, new GreaterThanEqualCriterionBuilder()); this.criterionBuilderMap.put(IS_EMPTY, new IsEmptyCriterionBuilder()); this.criterionBuilderMap.put(IS_NOT_EMPTY, new IsNotEmptyCriterionBuilder()); this.criterionBuilderMap.put(IS_NOT_NULL, new IsNotNullCriterionBuilder()); this.criterionBuilderMap.put(IS_NULL, new IsNullCriterionBuilder()); this.criterionBuilderMap.put(LESS_THAN, new LessThanCriterionBuilder()); this.criterionBuilderMap.put(LESS_THAN_OR_EQUAL, new LessThanEqualCriterionBuilder()); this.criterionBuilderMap.put(LIKE, new LikeCriterionBuilder(false)); this.criterionBuilderMap.put(LIKE_IC, new LikeCriterionBuilder(true)); this.criterionBuilderMap.put(NOT_EQUAL, new NotEqualCriterionBuilder()); } public Criteria build() throws QueryException { Matcher matcher = DYNA_QUERY_PATTERN.matcher(this.dynaQuery); if (!matcher.matches()) { throw new QueryException("The QueryMethod syntax is incorrect. It must start with 'findBy' " + ", findUniqueBy or 'countBy' expression followed by property expressions and operators.", this.dynaQuery); } Criteria criteria = this.createCriteria(matcher.group(1).equals("countBy")); String dynaQueryExpression = matcher.group(2); // order by Matcher orderByMatcher = ORDER_BY_PATTERN.matcher(dynaQueryExpression); if (orderByMatcher.find()) { dynaQueryExpression = StringUtils.remove(dynaQueryExpression, orderByMatcher.group()); String orderByProperty = normalizePropertyName(orderByMatcher.group(3)); String orderByDirection = orderByMatcher.group(4); Order orderBy = "Desc".equals(orderByDirection) ? Order.desc(orderByProperty) : Order.asc(orderByProperty); criteria.addOrder(orderBy); } // split properties String[] properties = DYNA_QUERY_OPERATOR_PATTERN.split(dynaQueryExpression); if (properties.length == 0 || properties.length > 2) { throw new QueryException(format( "The QueryMethod syntax is incorrect. Dynamic queries must have " + "at least one property an no more than two (yours has {0}).\nYou can use one of " + "the following logical operators ({1}) and these expression operators ({2})", properties.length, LOGICAL_OPERATOR_AND + ", " + LOGICAL_OPERATOR_OR, StringUtils.join(EXPRESSION_OPERATORS, ", ")), this.dynaQuery); } Matcher logicalOperatorsMatcher = DYNA_QUERY_OPERATOR_PATTERN.matcher(dynaQueryExpression); String logicalOperator = logicalOperatorsMatcher.find() ? logicalOperatorsMatcher.group(1) : LOGICAL_OPERATOR_AND; Junction junction = LOGICAL_OPERATOR_OR.equals(logicalOperator) ? Restrictions.disjunction() : Restrictions.conjunction(); for (String property : properties) { junction.add(this.createCriterion(property)); } criteria.add(junction); return criteria; } private Criteria createCriteria(boolean countQuery) { Criteria criteria = this.session.createCriteria(entityName); if (countQuery) { criteria.setProjection(Projections.rowCount()); } return criteria; } private Criterion createCriterion(final String property) { String propertyName = property; String criterionType = EQUAL; for (String expression : EXPRESSION_OPERATORS) { if (property.endsWith(expression)) { criterionType = expression; propertyName = StringUtils.remove(propertyName, criterionType); break; } } propertyName = normalizePropertyName(propertyName); return this.criterionBuilderMap.get(criterionType).build(propertyName); } /** * @param propertyName * @return */ private String normalizePropertyName(String propertyName) { propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1); return propertyName; } Object getNextPropertyValue() { return getPropertyValues(1)[0]; } Object[] getPropertyValues(int size) { if ((propertyValueIndex + size) <= this.queryParams.length) { Object[] values = ArrayUtils.subarray(this.queryParams, propertyValueIndex, propertyValueIndex + size); propertyValueIndex += size; return values; } else { throw new QueryException( format("The number of parameters ({0}) does not satisfy " + "QueryMethod expression", Integer.toString(this.queryParams.length)), this.dynaQuery); } } void validateQueryProperty(String propertyName, Object value) { if (!ArrayUtils.contains(this.entityMetadata.getPropertyNames(), propertyName)) { throw new QueryException( format("Error creating a QueryMethod, the property {0} is not " + "mapped for entity {1}.", propertyName, this.entityName), this.dynaQuery); } if (value != null) { Class propertyType = this.entityMetadata.getPropertyType(propertyName).getReturnedClass(); if (!propertyType.isAssignableFrom(value.getClass())) { throw new QueryException(format( "Error creating a QueryMethod, the property {0} type " + "({1}) is not compatible with the argument type ({2}).", propertyName, propertyType.getName(), value.getClass().getName()), this.dynaQuery); } } } /** * * @author Daniel Rochetti * @since 1.0 */ private interface CriterionBuilder { /** * * @param property * @return */ Criterion build(String property); } /** * * @author Daniel Rochetti * @since 1.0 */ private class BetweenCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value1 = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value1); Object value2 = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value2); return Restrictions.between(propertyName, value1, value2); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class EqualCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.eq(propertyName, value); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class GreaterThanCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.gt(propertyName, value); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class GreaterThanEqualCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.ge(propertyName, value); } } /** * * @see Restrictions#isEmpty(String) * * @author Daniel Rochetti * @since 1.0 */ private class IsEmptyCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { validateQueryProperty(propertyName, null); return Restrictions.isEmpty(propertyName); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class IsNotEmptyCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { validateQueryProperty(propertyName, null); return Restrictions.isNotEmpty(propertyName); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class IsNotNullCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { validateQueryProperty(propertyName, null); return Restrictions.isNotNull(propertyName); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class IsNullCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { validateQueryProperty(propertyName, null); return Restrictions.isNull(propertyName); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class LessThanCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.lt(propertyName, value); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class LessThanEqualCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.le(propertyName, value); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class LikeCriterionBuilder implements CriterionBuilder { private boolean ignoreCase; public LikeCriterionBuilder(boolean ignoreCase) { this.ignoreCase = ignoreCase; } public Criterion build(String propertyName) { Object propertyValue = DynaQueryBuilder.this.getNextPropertyValue(); if (!(propertyValue instanceof String)) { throw new QueryException( "Error creating QueryMethod. You can only use 'Like' " + "operator on String properties.", DynaQueryBuilder.this.dynaQuery); } validateQueryProperty(propertyName, propertyValue); return this.ignoreCase ? Restrictions.ilike(propertyName, (String) propertyValue) : Restrictions.like(propertyName, (String) propertyValue); } } /** * * @author Daniel Rochetti * @since 1.0 */ private class NotEqualCriterionBuilder implements CriterionBuilder { public Criterion build(String propertyName) { Object value = DynaQueryBuilder.this.getNextPropertyValue(); validateQueryProperty(propertyName, value); return Restrictions.ne(propertyName, value); } } }