From 7ff8a43a7558a6640292da7f86ad42503ea0d467 Mon Sep 17 00:00:00 2001 From: Phil Webb Date: Wed, 24 Aug 2011 17:18:09 +0100 Subject: [PATCH] DATACMNS-66 - Added support for IgnoreCase as query keyword. The query parsing subsystem now supports using IgnoreCase when referencing String parameters, e.g.: findByUsernameIgnoreCase(String username); Both 'IgnoreCase' and 'IgnoringCase' are supported. If you'd like to entirely ignore cases for all String property references add 'AllIgnoreCase' or 'AllIgnoringCase' to the query method. --- .../query/parser/AbstractQueryCreator.java | 2 +- .../query/parser/OrderBySource.java | 69 ++-- .../data/repository/query/parser/Part.java | 160 +++++---- .../repository/query/parser/PartTree.java | 314 +++++++++--------- .../query/parser/PartTreeUnitTests.java | 199 +++++++---- 5 files changed, 414 insertions(+), 330 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java index 96456073e..4fee1ec93 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java @@ -45,7 +45,7 @@ public abstract class AbstractQueryCreator { */ public AbstractQueryCreator(PartTree tree, ParameterAccessor parameters) { - Assert.notNull(tree); + Assert.notNull(tree, "PartTree must not be null"); this.tree = tree; this.parameters = parameters; diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java index 6bc2431d6..6eaec2e92 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/OrderBySource.java @@ -25,14 +25,11 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.util.StringUtils; - /** - * Simple helper class to create a {@link Sort} instance 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 ends are valid: {@code LastnameUsernameDesc}, - * {@code LastnameAscUsernameDesc}. - * + * Simple helper class to create a {@link Sort} instance 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 ends are valid: {@code LastnameUsernameDesc}, {@code LastnameAscUsernameDesc}. + * * @author Oliver Gierke */ public class OrderBySource { @@ -42,59 +39,71 @@ public class OrderBySource { private final List orders; - + /** + * Creates a new {@link OrderBySource} for the given String clause not doing any checks whether the referenced + * property actually exists. + * + * @param clause must not be {@literal null}. + */ public OrderBySource(String clause) { this(clause, null); } - + /** + * Creates a new {@link OrderBySource} for the given clause, checking the property referenced exists on the given + * type. + * + * @param clause must not be {@literal null}. + * @param domainClass + */ public OrderBySource(String clause, Class domainClass) { this.orders = new ArrayList(); - 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)); + throw new IllegalArgumentException(String.format("Invalid order syntax for part %s!", part)); } - Direction direction = Direction.fromString(matcher.group(2)); - this.orders.add(createOrder(matcher.group(1), direction, - domainClass)); + this.orders.add(createOrder(matcher.group(1), direction, domainClass)); } } - /** - * Creates an {@link Order} instance from the given property source, - * direction and domain class. If the domain class is given, we will use it - * for nested property traversal checks. - * + * Creates an {@link Order} instance from the given property source, direction and domain class. If the domain class + * is given, we will use it for nested property traversal checks. + * * @param propertySource * @param direction - * @param domainClass can be {@literal null}. + * @param domainClass can be {@literal null}. * @return * @see Property#from(String, Class) */ - private Order createOrder(String propertySource, Direction direction, - Class domainClass) { + private Order createOrder(String propertySource, Direction direction, Class domainClass) { if (null == domainClass) { - return new Order(direction, - StringUtils.uncapitalize(propertySource)); + return new Order(direction, StringUtils.uncapitalize(propertySource)); } - Property property = Property.from(propertySource, domainClass); return new Order(direction, property.toDotPath()); } - + /** + * Returns the clause as {@link Sort}. + * + * @return the {@link Sort} or null if no orders found. + */ public Sort toSort() { - return this.orders.isEmpty() ? null : new Sort(this.orders); } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Order By " + StringUtils.collectionToDelimitedString(orders, ", "); + } } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index e67009c20..acfaaa6ef 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -17,47 +17,73 @@ package org.springframework.data.repository.query.parser; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.util.StringUtils; - /** - * 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 + * 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 */ public class Part { + private static final Pattern IGNORE_CASE = Pattern.compile("Ignor(ing|e)Case"); + private final Property property; private final Part.Type type; + private boolean ignoreCase; /** - * Creates a new {@link Part} from the given method name part, the - * {@link Class} the part originates from and the start parameter index. - * + * Creates a new {@link Part} from the given method name part, the {@link Class} the part originates from and the + * start parameter index. + * * @param part * @param clazz */ public Part(String part, Class clazz) { + this(part, clazz, false); + } + + /** + * Creates a new {@link Part} from the given method name part, the {@link Class} the part originates from and the + * start parameter index. + * + * @param part + * @param clazz + * @param alwaysIgnoreCase + */ + public Part(String part, Class clazz, boolean alwaysIgnoreCase) { + + part = detectAndSetIgnoreCase(part); + this.ignoreCase = this.ignoreCase || alwaysIgnoreCase; this.type = Type.fromProperty(part, clazz); this.property = Property.from(type.extractProperty(part), clazz); } + private String detectAndSetIgnoreCase(String part) { + + Matcher matcher = IGNORE_CASE.matcher(part); + if (matcher.find()) { + ignoreCase = true; + part = part.substring(0, matcher.start()) + part.substring(matcher.end(), part.length()); + } + return part; + } public boolean getParameterRequired() { return getNumberOfArguments() > 0; } - /** * Returns how many method parameters are bound by this part. - * + * * @return */ public int getNumberOfArguments() { @@ -65,21 +91,40 @@ public class Part { return type.getNumberOfArguments(); } - /** - * @return the part + * @return the property */ public Property getProperty() { return property; } + /** + * @return the type + */ + public Part.Type getType() { + + return type; + } + + /** + * Returns whether the {@link Property} referenced should be matched ignoring case. + * + * @return + */ + public boolean shouldIgnoreCase() { + + if (!String.class.equals(getProperty().getType())) { + return false; + } + + return ignoreCase; + } /* - * (non-Javadoc) - * - * @see java.lang.Object#equals(java.lang.Object) - */ + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ @Override public boolean equals(Object obj) { @@ -92,17 +137,13 @@ public class Part { } Part that = (Part) obj; - - return this.property.equals(that.property) - && this.type.equals(that.type); + return this.property.equals(that.property) && this.type.equals(that.type); } - /* - * (non-Javadoc) - * - * @see java.lang.Object#hashCode() - */ + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ @Override public int hashCode() { @@ -112,31 +153,19 @@ public class Part { return result; } - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { return String.format("%s %s", property.getName(), type); } - /** - * @return the type - */ - public Part.Type getType() { - - return type; - } - - /** - * The type of a method name part. Used to create query parts in various - * ways. - * + * The type of a method name part. Used to create query parts in various ways. + * * @author Oliver Gierke */ public static enum Type { @@ -169,18 +198,16 @@ public class Part { // Need to list them again explicitly as the order is important // (esp. for IS_NULL, IS_NOT_NULL) - private static final List ALL = Arrays.asList(IS_NOT_NULL, - IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, - NOT_IN, IN, NEAR, WITHIN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); + private static final List ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, + NOT_LIKE, LIKE, NOT_IN, IN, NEAR, WITHIN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); + private List keywords; private int numberOfArguments; - /** - * Creates a new {@link Type} using the given keyword, number of - * arguments to be bound and operator. Keyword and operator can be - * {@literal null}. - * + * Creates a new {@link Type} using the given keyword, number of arguments to be bound and operator. Keyword and + * operator can be {@literal null}. + * * @param operator * @param numberOfArguments * @param keywords @@ -191,19 +218,16 @@ public class Part { this.keywords = Arrays.asList(keywords); } - private Type(String... keywords) { this(1, keywords); } - /** - * Returns the {@link Type} of the {@link Part} for the given raw - * property and the given {@link Class}. This will try to detect e.g. - * keywords contained in the raw property that trigger special query - * creation. Returns {@link #SIMPLE_PROPERTY} by default. - * + * Returns the {@link Type} of the {@link Part} for the given raw property and the given {@link Class}. 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 clazz * @return @@ -219,13 +243,10 @@ public class Part { return SIMPLE_PROPERTY; } - /** - * 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. - * + * 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 clazz * @return @@ -245,11 +266,9 @@ public class Part { return false; } - /** - * Returns the number of arguments the property binds. By default this - * exactly one argument. - * + * Returns the number of arguments the property binds. By default this exactly one argument. + * * @return */ public int getNumberOfArguments() { @@ -257,24 +276,21 @@ public class Part { return numberOfArguments; } - /** - * Callback method to extract the actual property to be bound from the - * given part. Strips the keyword from the part's end if available. - * + * 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) { String candidate = StringUtils.uncapitalize(part); - for (String keyword : keywords) { if (candidate.endsWith(keyword)) { return candidate.substring(0, candidate.indexOf(keyword)); } } - return candidate; } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index 830489078..d3e0b9929 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -15,9 +15,6 @@ */ package org.springframework.data.repository.query.parser; -import static java.lang.String.*; -import static java.util.regex.Pattern.*; - import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -29,241 +26,242 @@ import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.util.Assert; import org.springframework.util.StringUtils; - /** - * Class to parse a {@link String} into a tree or {@link OrPart}s consisting of - * simple {@link Part} instances in turn. Takes a domain class as well to - * validate that each of the {@link Part}s are refering to a property of the - * domain class. The {@link PartTree} can then be used to build queries based on - * its API instead of parsing the method name for each query execution. - * + * Class to parse a {@link String} into a tree or {@link OrPart}s consisting of simple {@link Part} instances in turn. + * Takes a domain class as well to validate that each of the {@link Part}s are referring to a property of the domain + * class. The {@link PartTree} can then be used to build queries based on its API instead of parsing the method name for + * each query execution. + * * @author Oliver Gierke */ public class PartTree implements Iterable { - private static final String ORDER_BY = "OrderBy"; + private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get)(\\p{Upper}.*?)??By"); private static final String KEYWORD_TEMPLATE = "(%s)(?=[A-Z])"; - private static final String DISTINCT = "Distinct"; - - private static final Pattern PREFIX_TEMPLATE = Pattern - .compile("^(find|read|get)(\\p{Upper}.*?)??By"); - - private final boolean distinct; - private final OrderBySource orderBySource; - private final List nodes = new ArrayList(); - /** - * Creates a new {@link PartTree} by parsing the given {@link String} - * - * @param source the {@link String} to parse - * @param domainClass the domain class to check indiviual parts against to - * ensure they refer to a property of the class + * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser". + */ + private final Subject subject; + + /** + * The subject, for example "findDistinctUserByNameOrderByAge" would have the predicate "NameOrderByAge". + */ + private final Predicate predicate; + + /** + * Creates a new {@link PartTree} by parsing the given {@link String}. + * + * @param source the {@link String} to parse + * @param domainClass the domain class to check individual parts against to ensure they refer to a property of the + * class */ public PartTree(String source, Class domainClass) { - Assert.notNull(source); - Assert.notNull(domainClass); + Assert.notNull(source, "Source must not be null"); + Assert.notNull(domainClass, "DomainClass must not be null"); - this.distinct = detectDistinct(source); - String foo = strip(source); - String[] parts = split(foo, ORDER_BY); - - if (parts.length > 2) { - throw new IllegalArgumentException( - "OrderBy must not be used more than once in a method name!"); + Matcher matcher = PREFIX_TEMPLATE.matcher(source); + if (!matcher.find()) { + this.subject = new Subject(null); + this.predicate = new Predicate(source, domainClass); + } else { + this.subject = new Subject(matcher.group(2)); + this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass); } - - buildTree(parts[0], domainClass); - this.orderBySource = - parts.length == 2 ? new OrderBySource(parts[1], domainClass) - : null; - } - /* - * (non-Javadoc) - * - * @see java.lang.Iterable#iterator() - */ + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ public Iterator iterator() { - - return nodes.iterator(); + return predicate.iterator(); } - - private void buildTree(String source, Class domainClass) { - - String[] split = split(source, "Or"); - - for (String part : split) { - nodes.add(new OrPart(part, domainClass)); - } - } - - /** - * Returns the {@link Sort} specification parsed from the source. - * - * @return + * Returns the {@link Sort} specification parsed from the source or null. + * + * @return the sort */ public Sort getSort() { + OrderBySource orderBySource = predicate.getOrderBySource(); return orderBySource == null ? null : orderBySource.toSort(); } - /** * Returns whether we indicate distinct lookup of entities. - * - * @return + * + * @return true if distinct */ public boolean isDistinct() { - return distinct; + return subject.isDistinct(); } - /** - * Returns an {@link Iterable} of all parts contained in the - * {@link PartTree}. - * - * @return + * Returns an {@link Iterable} of all parts contained in the {@link PartTree}. + * + * @return the iterable {@link Part}s */ public Iterable getParts() { List result = new ArrayList(); - for (OrPart orPart : this) { for (Part part : orPart) { result.add(part); } } - return result; } - /** - * 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 + * Returns true if all String based selections should not consider sentence case. + * + * @return true if case is ignored */ - private static String[] split(String text, String keyword) { + public boolean shouldAlwaysIgnoreCase() { - String regex = format(KEYWORD_TEMPLATE, keyword); - - Pattern pattern = compile(regex); - return pattern.split(text); + return predicate.shouldAlwaysIgnoreCase(); } - - /** - * Strips a prefix from the given method name if it starts with one of - * {@value #PREFIXES}. - * - * @param methodName - * @return - */ - private String strip(String methodName) { - - Matcher matcher = PREFIX_TEMPLATE.matcher(methodName); - - if (matcher.find()) { - return methodName.substring(matcher.group().length()); - } else { - return methodName; - } - } - - - /** - * Checks whether the given source string contains the {@link #DISTINCT} - * keyword in it's prefix. - * - * @param source - * @return - */ - private boolean detectDistinct(String source) { - - Matcher matcher = PREFIX_TEMPLATE.matcher(source); - - if (!matcher.find()) { - return false; - } - - String group = matcher.group(2); - return group != null && group.contains(DISTINCT); - } - - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ @Override public String toString() { - return String.format("%s %s", - StringUtils.collectionToDelimitedString(nodes, " or "), - orderBySource.toString()); + OrderBySource orderBySource = predicate.getOrderBySource(); + return String.format("%s%s", StringUtils.collectionToDelimitedString(predicate.nodes, " or "), + (orderBySource == null ? "" : " " + orderBySource)); } /** - * A part of the parsed source that results from splitting up the resource - * ar {@literal Or} keywords. Consists of {@link Part}s that have to be - * concatenated by {@literal And}. - * - * @author Oliver Gierke + * Splits the given text at the given keywords. Expects camel-case style to only match concrete keywords and not + * derivatives of it. + * + * @param text the text to split + * @param keyword the keyword to split around + * @return an arry of split items + */ + private static String[] split(String text, String keyword) { + + Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword)); + return pattern.split(text); + } + + /** + * A part of the parsed source that results from splitting up the resource around {@literal Or} keywords. Consists of + * {@link Part}s that have to be concatenated by {@literal And}. */ public static class OrPart implements Iterable { private final List children = new ArrayList(); - /** * Creates a new {@link OrPart}. - * - * @param source the source to split up into {@literal And} parts in - * turn. - * @param domainClass the domain class to check the resulting - * {@link Part}s against. + * + * @param source the source to split up into {@literal And} parts in turn. + * @param domainClass the domain class to check the resulting {@link Part}s against. + * @param alwaysIgnoreCase if always ignoring case */ - OrPart(String source, Class domainClass) { + OrPart(String source, Class domainClass, boolean alwaysIgnoreCase) { String[] split = split(source, "And"); - for (String part : split) { - children.add(new Part(part, domainClass)); + children.add(new Part(part, domainClass, alwaysIgnoreCase)); } } + public Iterator iterator() { + + return children.iterator(); + } - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ @Override public String toString() { return StringUtils.collectionToDelimitedString(children, " and "); } + } + /** + * Represents the subject part of the query. E.g. {@code findDistinctUserByNameOrderByAge} would have the subject + * {@code DistinctUser}. + * + * @author Phil Webb + */ + private static class Subject { - /* - * (non-Javadoc) - * - * @see java.lang.Iterable#iterator() - */ - public Iterator iterator() { + private static final String DISTINCT = "Distinct"; - return children.iterator(); + private boolean distinct; + + public Subject(String subject) { + this.distinct = (subject == null ? false : subject.contains(DISTINCT)); + } + + public boolean isDistinct() { + return distinct; + } + } + + /** + * Represents the predicate part of the query. + * + * @author Oliver Gierke + * @author Phil Webb + */ + private static class Predicate { + + private static Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case"); + private static final String ORDER_BY = "OrderBy"; + + private final List nodes = new ArrayList(); + private OrderBySource orderBySource; + private boolean alwaysIgnoreCase; + + public Predicate(String predicate, Class domainClass) { + + predicate = detectAndSetAllIgnoreCase(predicate); + String[] parts = split(predicate, ORDER_BY); + + if (parts.length > 2) { + throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!"); + } + + buildTree(parts[0], domainClass); + this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], domainClass) : null; + } + + private String detectAndSetAllIgnoreCase(String predicate) { + + Matcher matcher = ALL_IGNORE_CASE.matcher(predicate); + + if (matcher.find()) { + alwaysIgnoreCase = true; + predicate = predicate.substring(0, matcher.start()) + predicate.substring(matcher.end(), predicate.length()); + } + + return predicate; + } + + private void buildTree(String source, Class domainClass) { + + String[] split = split(source, "Or"); + for (String part : split) { + nodes.add(new OrPart(part, domainClass, alwaysIgnoreCase)); + } + } + + public Iterator iterator() { + return nodes.iterator(); + } + + public OrderBySource getOrderBySource() { + return orderBySource; + } + + public boolean shouldAlwaysIgnoreCase() { + return alwaysIgnoreCase; } } } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java index c48d3714c..62827c945 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -1,12 +1,12 @@ /* * 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 @@ -15,8 +15,10 @@ */ package org.springframework.data.repository.query.parser; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; import java.util.Iterator; @@ -26,140 +28,200 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree.OrPart; - /** * Unit tests for {@link PartTree}. * * @author Oliver Gierke + * @author Phil Webb */ public class PartTreeUnitTests { + private String[] PREFIXES = { "find", "read", "get" }; + @Test(expected = IllegalArgumentException.class) public void rejectsNullSource() throws Exception { - new PartTree(null, getClass()); } - @Test(expected = IllegalArgumentException.class) public void rejectsNullDomainClass() throws Exception { - new PartTree("test", null); } - @Test(expected = IllegalArgumentException.class) public void rejectsMultipleOrderBy() throws Exception { - - new PartTree("firstnameOrderByLastnameOrderByFirstname", User.class); + partTree("firstnameOrderByLastnameOrderByFirstname"); } - @Test public void parsesSimplePropertyCorrectly() throws Exception { - - PartTree partTree = new PartTree("firstname", User.class); - assertPart(partTree, new Part[]{new Part("firstname", User.class)}); + PartTree partTree = partTree("firstname"); + assertPart(partTree, parts("firstname")); } - @Test public void parsesAndPropertiesCorrectly() throws Exception { - - PartTree partTree = new PartTree("firstnameAndLastname", User.class); - assertPart(partTree, new Part[]{new Part("firstname", User.class), - new Part("lastname", User.class)}); + PartTree partTree = partTree("firstnameAndLastname"); + assertPart(partTree, parts("firstname", "lastname")); assertThat(partTree.getSort(), is(nullValue())); } - @Test public void parsesOrPropertiesCorrectly() throws Exception { - - PartTree partTree = new PartTree("firstnameOrLastname", User.class); - assertPart(partTree, new Part[]{new Part("firstname", User.class)}, - new Part[]{new Part("lastname", User.class)}); + PartTree partTree = partTree("firstnameOrLastname"); + assertPart(partTree, parts("firstname"), parts("lastname")); assertThat(partTree.getSort(), is(nullValue())); } - @Test public void parsesCombinedAndAndOrPropertiesCorrectly() throws Exception { - - PartTree tree = - new PartTree("firstnameAndLastnameOrLastname", User.class); - assertPart(tree, new Part[]{new Part("firstname", User.class), - new Part("lastname", User.class)}, new Part[]{new Part( - "lastname", User.class)}); + PartTree tree = partTree("firstnameAndLastnameOrLastname"); + assertPart(tree, parts("firstname", "lastname"), parts("lastname")); } - @Test public void hasSortIfOrderByIsGiven() throws Exception { - - PartTree partTree = - new PartTree("firstnameOrderByLastnameDesc", User.class); + PartTree partTree = partTree("firstnameOrderByLastnameDesc"); assertThat(partTree.getSort(), is(new Sort(Direction.DESC, "lastname"))); } + @Test + public void hasSortIfOrderByIsGivenWithAllIgnoreCase() throws Exception { + hasSortIfOrderByIsGivenWithAllIgnoreCase("firstnameOrderByLastnameDescAllIgnoreCase"); + hasSortIfOrderByIsGivenWithAllIgnoreCase("firstnameOrderByLastnameDescAllIgnoringCase"); + hasSortIfOrderByIsGivenWithAllIgnoreCase("firstnameAllIgnoreCaseOrderByLastnameDesc"); + } + + private void hasSortIfOrderByIsGivenWithAllIgnoreCase(String source) throws Exception { + PartTree partTree= partTree(source); + assertThat(partTree.getSort(), is(new Sort(Direction.DESC, "lastname"))); + } @Test public void detectsDistinctCorrectly() throws Exception { - - PartTree tree = new PartTree("findDistinctByLastname", User.class); - assertThat(tree.isDistinct(), is(true)); - - tree = new PartTree("findUsersDistinctByLastname", User.class); - assertThat(tree.isDistinct(), is(true)); - - tree = new PartTree("findDistinctUsersByLastname", User.class); - assertThat(tree.isDistinct(), is(true)); - - tree = new PartTree("findUsersByLastname", User.class); - assertThat(tree.isDistinct(), is(false)); - - tree = new PartTree("findByLastname", User.class); - assertThat(tree.isDistinct(), is(false)); - - // Check it's non-greedy (would strip everything until Order*By* - // otherwise) - tree = new PartTree("findByLastnameOrderByFirstnameDesc", User.class); - assertThat(tree.isDistinct(), is(false)); - assertThat(tree.getSort(), is(new Sort(Direction.DESC, "firstname"))); + for (String prefix : PREFIXES) { + detectsDistinctCorrectly(prefix + "DistinctByLastname", true); + detectsDistinctCorrectly(prefix + "UsersDistinctByLastname", true); + detectsDistinctCorrectly(prefix + "DistinctUsersByLastname", true); + detectsDistinctCorrectly(prefix + "UsersByLastname", false); + detectsDistinctCorrectly(prefix + "ByLastname", false); + // Check it's non-greedy (would strip everything until Order*By* + // otherwise) + PartTree tree = detectsDistinctCorrectly(prefix + "ByLastnameOrderByFirstnameDesc", false); + assertThat(tree.getSort(), is(new Sort(Direction.DESC, "firstname"))); + } } + private PartTree detectsDistinctCorrectly(String source, boolean expected) { + PartTree tree = partTree(source); + assertThat("Unexpected distinct value for '" + source + "'", tree.isDistinct(), is(expected)); + return tree; + } @Test public void parsesWithinCorrectly() { - PartTree tree = new PartTree("findByLocationWithin", User.class); + PartTree tree = partTree("findByLocationWithin"); for (Part part : tree.getParts()) { assertThat(part.getType(), is(Type.WITHIN)); - assertThat(part.getProperty(), is(new Property("location", User.class))); + assertThat(part.getProperty(), is(newProperty("location"))); } } @Test public void parsesNearCorrectly() { - PartTree tree = new PartTree("findByLocationNear", User.class); + PartTree tree = partTree("findByLocationNear"); for (Part part : tree.getParts()) { assertThat(part.getType(), is(Type.NEAR)); - assertThat(part.getProperty(), is(new Property("location", User.class))); + assertThat(part.getProperty(), is(newProperty("location"))); } } - private void assertPart(PartTree tree, Part[]... parts) { + @Test + public void supportToStringWithoutSortOrder() throws Exception { + PartTree tree = partTree("firstname"); + assertThat(tree.toString(), is(equalTo("firstname SIMPLE_PROPERTY"))); + } + @Test + public void supportToStringWithSortOrder() throws Exception { + PartTree tree = partTree("firstnameOrderByLastnameDesc"); + assertThat(tree.toString(), is(equalTo("firstname SIMPLE_PROPERTY Order By lastname: DESC"))); + } + + @Test + public void detectsIgnoreAllCase() throws Exception { + detectsIgnoreAllCase("firstnameOrderByLastnameDescAllIgnoreCase",true); + detectsIgnoreAllCase("firstnameOrderByLastnameDescAllIgnoringCase",true); + detectsIgnoreAllCase("firstnameAllIgnoreCaseOrderByLastnameDesc",true); + detectsIgnoreAllCase("getByFirstnameAllIgnoreCase",true); + detectsIgnoreAllCase("getByFirstname",false); + detectsIgnoreAllCase("firstnameOrderByLastnameDesc",false); + } + + private void detectsIgnoreAllCase(String source, boolean expected) throws Exception { + PartTree tree = partTree(source); + assertThat(tree.shouldAlwaysIgnoreCase(), is(expected)); + for (Part part : tree.getParts()) { + assertThat(part.shouldIgnoreCase(), is(expected)); + } + } + + @Test + public void detectsSpecificIgnoreCase() throws Exception { + PartTree tree = partTree("findByFirstnameIgnoreCaseAndLastname"); + assertPart(tree, parts("firstname","lastname")); + Iterator parts = tree.getParts().iterator(); + assertThat(parts.next().shouldIgnoreCase(), is(true)); + assertThat(parts.next().shouldIgnoreCase(), is(false)); + } + + @Test + public void detectsSpecificIgnoringCase() throws Exception { + PartTree tree = partTree("findByFirstnameIgnoringCaseAndLastname"); + assertPart(tree, parts("firstname","lastname")); + Iterator parts = tree.getParts().iterator(); + assertThat(parts.next().shouldIgnoreCase(), is(true)); + assertThat(parts.next().shouldIgnoreCase(), is(false)); + } + + @Test + public void doesNotIgnoreCaseIfNotStringProperty() throws Exception { + PartTree tree = partTree("findByLocationIgnoringCase"); + assertPart(tree, parts("location")); + Iterator parts = tree.getParts().iterator(); + assertThat(parts.next().shouldIgnoreCase(), is(false)); + } + + private PartTree partTree(String source) { + return new PartTree(source, User.class); + } + + private Part part(String part) { + return new Part(part, User.class); + } + + private Part[] parts(String... part) { + Part[] parts = new Part[part.length]; + for (int i = 0; i < parts.length; i++) { + parts[i] = part(part[i]); + } + return parts; + } + + private Property newProperty(String name) { + return new Property(name, User.class); + } + + private void assertPart(PartTree tree, Part[]... parts) { Iterator iterator = tree.iterator(); for (Part[] part : parts) { assertThat(iterator.hasNext(), is(true)); Iterator partIterator = iterator.next().iterator(); for (int k = 0; k < part.length; k++) { - assertThat(String.format("Expected %d parts but have %d", - part.length, k + 1), partIterator.hasNext(), is(true)); + assertThat(String.format("Expected %d parts but have %d", part.length, k), partIterator.hasNext(), + is(true)); Part next = partIterator.next(); - assertThat( - String.format("Expected %s but got %s!", part[k], next), - part[k], is(next)); + assertThat(String.format("Expected %s but got %s!", part[k], next), part[k], is(next)); } assertThat("Too many parts!", partIterator.hasNext(), is(false)); } @@ -167,7 +229,6 @@ public class PartTreeUnitTests { } class User { - String firstname; String lastname; double[] location;