diff --git a/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index 49b37895c..0dddbf757 100644 --- a/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -50,9 +50,11 @@ public class PartTree implements Iterable { * @see http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#ubc */ private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))"; + private static final String QUERY_PATTERN = "find|read|get|query"; + private static final String COUNT_PATTERN = "count"; private static final String DELETE_PATTERN = "delete|remove"; - private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get|count|query|" + DELETE_PATTERN - + ")(\\p{Lu}.*?)??By"); + private static final Pattern PREFIX_TEMPLATE = Pattern.compile( // + "^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By"); /** * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser". @@ -133,6 +135,26 @@ public class PartTree implements Iterable { return subject.isDelete(); } + /** + * Return {@literal true} if the create {@link PartTree} is meant to be used for a query with limited maximal results. + * + * @return + * @since 1.9 + */ + public boolean isLimiting() { + return getMaxResults() != null; + } + + /** + * Return the number of maximal results to return or {@literal null} if not restricted. + * + * @return + * @since 1.9 + */ + public Integer getMaxResults() { + return subject.getMaxResults(); + } + /** * Returns an {@link Iterable} of all parts contained in the {@link PartTree}. * @@ -234,22 +256,48 @@ public class PartTree implements Iterable { * @author Phil Webb * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ private static class Subject { private static final String DISTINCT = "Distinct"; private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By"); private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(" + DELETE_PATTERN + ")(\\p{Lu}.*?)??By"); + private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\\d*)?"; + private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern.compile("^(" + QUERY_PATTERN + ")(" + DISTINCT + ")?" + + LIMITING_QUERY_PATTERN + "(\\p{Lu}.*?)??By"); private final boolean distinct; private final boolean count; private final boolean delete; + private final Integer maxResults; public Subject(String subject) { this.distinct = subject == null ? false : subject.contains(DISTINCT); this.count = matches(subject, COUNT_BY_TEMPLATE); this.delete = matches(subject, DELETE_BY_TEMPLATE); + this.maxResults = returnMaxResultsIfFirstKSubjectOrNull(subject); + } + + /** + * @param subject + * @return + * @since 1.9 + */ + private Integer returnMaxResultsIfFirstKSubjectOrNull(String subject) { + + if (subject == null) { + return null; + } + + Matcher grp = LIMITED_QUERY_TEMPLATE.matcher(subject); + + if (!grp.find()) { + return null; + } + + return StringUtils.hasText(grp.group(4)) ? Integer.valueOf(grp.group(4)) : 1; } /** @@ -270,6 +318,10 @@ public class PartTree implements Iterable { return distinct; } + public Integer getMaxResults() { + return maxResults; + } + private final boolean matches(String subject, Pattern pattern) { return subject == null ? false : pattern.matcher(subject).find(); } @@ -330,5 +382,4 @@ public class PartTree implements Iterable { return orderBySource; } } - } diff --git a/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java index aed7498f1..40579992f 100644 --- a/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -553,6 +553,118 @@ public class PartTreeUnitTests { assertThat(tree.isDelete(), is(true)); } + /** + * @see DATACMNS-516 + */ + @Test + public void disablesFindFirstKImplicitIfNotPresent() { + assertLimiting("findByLastname", User.class, false, null); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstImplicit() { + assertLimiting("findFirstByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirst1Explicit() { + assertLimiting("findFirstByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKExplicit() { + assertLimiting("findFirst10ByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKUsersExplicit() { + assertLimiting("findFirst10UsersByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKDistinctUsersExplicit() { + assertLimiting("findFirst10DistinctUsersByLastname", User.class, true, 10, true); + assertLimiting("findDistinctFirst10UsersByLastname", User.class, true, 10, true); + assertLimiting("findFirst10UsersDistinctByLastname", User.class, true, 10, true); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopImplicit() { + assertLimiting("findTopByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTop1Explicit() { + assertLimiting("findTop1ByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKExplicit() { + assertLimiting("findTop10ByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKUsersExplicit() { + assertLimiting("findTop10UsersByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKDistinctUsersExplicit() { + assertLimiting("findTop10DistinctUsersByLastname", User.class, true, 10, true); + assertLimiting("findDistinctTop10UsersByLastname", User.class, true, 10, true); + assertLimiting("findTop10UsersDistinctByLastname", User.class, true, 10, true); + } + + @Test + public void shouldNotSupportLimitingCountQueries() { + assertLimiting("countFirst10DistinctUsersByLastname", User.class, false, null, true); + assertLimiting("countTop10DistinctUsersByLastname", User.class, false, null, true); + } + + private static void assertLimiting(String methodName, Class entityType, boolean limiting, Integer maxResults) { + assertLimiting(methodName, entityType, limiting, maxResults, false); + } + + private static void assertLimiting(String methodName, Class entityType, boolean limiting, Integer maxResults, + boolean distinct) { + + PartTree tree = new PartTree(methodName, entityType); + + assertThat(tree.isLimiting(), is(limiting)); + assertThat(tree.getMaxResults(), is(maxResults)); + assertThat(tree.isDistinct(), is(distinct)); + } + private static void assertType(Iterable sources, Type type, String property) { assertType(sources, type, property, 1, true); }