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.
This commit is contained in:
Phil Webb
2011-08-24 17:18:09 +01:00
committed by Oliver Gierke
parent 651a1f2db2
commit 7ff8a43a75
5 changed files with 414 additions and 330 deletions

View File

@@ -45,7 +45,7 @@ public abstract class AbstractQueryCreator<T, S> {
*/
public AbstractQueryCreator(PartTree tree, ParameterAccessor parameters) {
Assert.notNull(tree);
Assert.notNull(tree, "PartTree must not be null");
this.tree = tree;
this.parameters = parameters;

View File

@@ -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<Order> 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<Sort.Order>();
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, ", ");
}
}

View File

@@ -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<Part.Type> 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<Part.Type> 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<String> 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;
}
}

View File

@@ -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<OrPart> {
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<OrPart> nodes = new ArrayList<PartTree.OrPart>();
/**
* 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<OrPart> 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 <tt>null</tt>.
*
* @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 <tt>true<tt> 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<Part> getParts() {
List<Part> result = new ArrayList<Part>();
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 <tt>true</tt> if all String based selections should not consider sentence case.
*
* @return <tt>true</tt> 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<Part> {
private final List<Part> children = new ArrayList<Part>();
/**
* 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<Part> 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<Part> 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<OrPart> nodes = new ArrayList<OrPart>();
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<OrPart> iterator() {
return nodes.iterator();
}
public OrderBySource getOrderBySource() {
return orderBySource;
}
public boolean shouldAlwaysIgnoreCase() {
return alwaysIgnoreCase;
}
}
}

View File

@@ -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<Part> 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<Part> 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<Part> 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<OrPart> iterator = tree.iterator();
for (Part[] part : parts) {
assertThat(iterator.hasNext(), is(true));
Iterator<Part> 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;