diff --git a/README.md b/README.md
index cf82a000..2d68c39e 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[![Donae](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/mmnaseri)
[![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.mmnaseri.utils/spring-data-mock/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.mmnaseri.utils/spring-data-mock)
-[![Dependency Status](https://www.versioneye.com/user/projects/5709ee7dfcd19a00415b101a/badge.svg?style=flat)](https://www.versioneye.com/user/projects/5709ee7dfcd19a00415b101a)
+[![Dependency Status](https://www.versioneye.com/user/projects/5722a8f5ba37ce0031fc17f0/badge.svg?style=flat)](https://www.versioneye.com/user/projects/5722a8f5ba37ce0031fc17f0)
[![Build Status](https://travis-ci.org/mmnaseri/spring-data-mock.svg?branch=master)](https://travis-ci.org/mmnaseri/spring-data-mock)
[![Codacy Badge](https://api.codacy.com/project/badge/grade/ad9f174fa0654a2b8c925b86973f272d)](https://www.codacy.com/app/mmnaseri/spring-data-mock)
[![Coverage Status](https://coveralls.io/repos/github/mmnaseri/spring-data-mock/badge.svg?branch=master)](https://coveralls.io/github/mmnaseri/spring-data-mock?branch=master)
@@ -39,8 +39,30 @@ or you can add a maven dependency since it is now available in Maven central:
com.mmnaseri.utilsspring-data-mock${latest-version}
+ test
+#### Note on Dependencies
+
+*Spring Data Mock* depends on [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/) to log
+all the interactions with the framework. If you need to, you can exclude this dependency from the framework by
+using [Maven exclusions](https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html#Dependency_Exclusions):
+
+```xml
+
+ com.mmnaseri.utils
+ spring-data-mock
+ ${latest-version}
+ test
+
+
+ commons-logging
+ commons-logging
+
+
+
+```
+
## Quick Start
Regardless of how you add the necessary dependency to your project, mocking a repository can be as simple as:
@@ -49,6 +71,20 @@ Regardless of how you add the necessary dependency to your project, mocking a re
where `builder()` is a static method of the `RepositoryFactoryBuilder` class under package `com.mmnaseri.utils.spring.data.dsl.factory`.
+Example:
+
+```java
+import com.mmnaseri.utils.spring.data.dsl.factory.RepositoryFactoryBuilder;
+
+public class CustomerRepositoryTest {
+
+ @Test
+ public void testDemo() {
+
+ final CustomerRepository repository = RepositoryFactoryBuilder.builder().mock(CustomerRepository.class);
+ repository.save(new Customer());
+```
+
An alternate way of mocking a repository would be by using the `RepositoryMockBuilder` class under the `com.mmnaseri.utils.spring.data.dsl.mock`
package:
diff --git a/spring-data-mock-build/pom.xml b/spring-data-mock-build/pom.xml
index 0e4a0b34..1099bf20 100644
--- a/spring-data-mock-build/pom.xml
+++ b/spring-data-mock-build/pom.xml
@@ -26,7 +26,7 @@
com.mmnaseri.utilsspring-data-mock-build
- 1.0.2
+ 1.1Spring Data Mock: Build AggregatorThis is the build module that will aggregate all reactors for the spring-data-mock projecthttps://mmnaseri.github.io/spring-data-mock
diff --git a/spring-data-mock/pom.xml b/spring-data-mock/pom.xml
index b96dd0a5..63c85392 100644
--- a/spring-data-mock/pom.xml
+++ b/spring-data-mock/pom.xml
@@ -26,7 +26,7 @@
com.mmnaseri.utilsspring-data-mock
- 1.0.3
+ 1.1Spring Data MockA framework for mocking Spring Data repositorieshttps://mmnaseri.github.io/spring-data-mock
@@ -80,6 +80,34 @@
+
+ com.querydsl
+ querydsl-core
+ ${querydsl.version}
+ provided
+
+
+ com.querydsl
+ querydsl-apt
+ ${querydsl.version}
+ provided
+
+
+ com.querydsl
+ querydsl-collections
+ ${querydsl.version}
+
+
+ cglib
+ cglib-nodep
+ ${cglib.version}
+ provided
+
+
+ org.slf4j
+ slf4j-log4j12
+ 1.6.1
+ commons-loggingcommons-logging
@@ -125,6 +153,13 @@
joda-timejoda-time${joda-time.version}
+ provided
+
+
+ javax.persistence
+ persistence-api
+ ${persistence-api.version}
+ provided
@@ -270,16 +305,19 @@
1.12.1.RELEASE1.8.1.RELEASE1.10.1.RELEASE
- 2.7.3
+ 2.7.46.9.101.3
- 2.9.3
+ 2.9.41.62.10.33.0.02.8.21.6.72.7
+ 4.1.0
+ 3.2.2
+ 1.0.2
\ No newline at end of file
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractor.java
new file mode 100644
index 00000000..c587fd4f
--- /dev/null
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractor.java
@@ -0,0 +1,399 @@
+package com.mmnaseri.utils.spring.data.domain.impl;
+
+import com.mmnaseri.utils.spring.data.domain.*;
+import com.mmnaseri.utils.spring.data.error.QueryParserException;
+import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
+import com.mmnaseri.utils.spring.data.query.*;
+import com.mmnaseri.utils.spring.data.query.impl.*;
+import com.mmnaseri.utils.spring.data.string.DocumentReader;
+import com.mmnaseri.utils.spring.data.string.impl.DefaultDocumentReader;
+import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
+import com.mmnaseri.utils.spring.data.tools.StringUtils;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ *
This class will parse a query method's name and extract a {@link com.mmnaseri.utils.spring.data.dsl.factory.QueryDescription query description}
+ * from that name.
+ *
+ *
In parsing the name, words are considered as being tokens in a camel case name.
+ *
+ *
Here is how a query method's name is parsed:
+ *
+ *
+ *
We will look at the first word in the name until we reach one of the keywords that says we are specifying a limit, or
+ * we are going over the criteria defined by the method. This prefix will be called the "function name" for the operation
+ * defined by the query method. If the function name is one of "read", "find", "query", "get", "load", or "select", we will
+ * set the function name to {@literal null} to indicate that no special function should be applied to the result set. We
+ * are only looking at the first word to let you be more verbose about the purpose of your query (e.g.
+ * {@literal findAllGreatPeopleByGreatnessGreaterThan(Integer greatness)} will still resolve to the function
+ * {@literal find}, which will ultimately be returned as {@literal null}
+ *
We will then look for any of these patterns:
+ *
+ *
The word {@literal By}, signifying that we are ready to start parsing the query criteria
+ *
One of the words {@literal First} or {@literal Top}, signifying that we should look for a limit on the number
+ * of results returned.
+ *
The word {@literal Distinct}, signifying that the results should include no duplicates.
+ *
+ *
+ *
If the word {@literal First} had appeared, we will see if it is followed by an integer. If it is, that will be the limit.
+ * If not, a limit of {@literal 1} is assumed.
+ *
If the word {@literal Top} had appeared, we will look for the limit number, which should be an integer value.
+ *
At this point, we will continue until we see 'By'. In the above, steps, we will look for the keywords in any order,
+ * and there can be any words in between. So, {@literal getTop5StudentsWhoAreAwesomeDistinct} is the same as {@literal getTop5Distinct}
+ *
Once we reach the word "By", we will read the query in terms of "decision branches". Branches are separated using the keyword
+ * "Or", and each branch is a series of conjunctions. So, while you are separating your conditions with "And", you are in the same branch.
+ *
A single branch consists of the pattern: "(Property)(Operator)?((And)(Property)(Operator)?)*". If the operator is missing, "Is" is assumed.
+ * Properties must match a proper property in the domain object. So, if you have "AddressZipPrefix" in your query method name, there must be a property
+ * reachable by one of the following paths in your domain class (in the given order):
+ *
+ *
{@literal addressZipPrefix}
+ *
{@literal addressZip.prefix}
+ *
{@literal address.zipPrefix}
+ *
{@literal address.zip.prefix}
+ *
+ * Note that if you have both the "addressZip" and "address.zip" in your entity, the first will be taken up. To force the parser to choose the former, use
+ * the underscore character ({@literal _}) in place of the dot, like so: "{@literal Address_Zip}"
+ * Depending on the operator that was matched to the suffix provided (e.g. GreaterThan, Is, etc.), a given number of method parameters will be matched
+ * as the operands to that operator. For instance, "Is" requires two values to determine equality, one if the property found on the domain object, and
+ * the other must be provided by the query method.
+ * The operators themselves are scanned eagerly and based on the set of operators defined in the {@link OperatorContext}.
+ *
+ *
We continue the pattern indicated above, until we reach the end of the method name, or we reach the "OrderBy" pattern. Once we see "OrderBy"
+ * we expect the following pattern: "((Property)(Direction))+", wherein "Property" must follow the same rule as above, and "Direction" is one of
+ * "Asc" and "Desc" to indicate "ascending" and "descending" ordering, respectively.
+ *
Finally, we look to see if the keyword "AllIgnoreCase" or {@link #ALL_IGNORE_CASE_SUFFIX one of its variations} is present at the end of the
+ * query name, which will indicate all applicable comparisons should be case-insensitive.
+ *
At the end, we allow one additional parameter for the query method, which can be of either of these types:
+ *
+ *
{@link Sort Sort}: to indicate a dynamic sort defined at runtime. If a static sort is already indicated via the pattern above, this will
+ * result in an error.
+ *
{@link Pageable Pageable}: to indicate a paging (and, possibly, sorting) at runtime. If a static sort is already indicated via the pattern
+ * above, the sort portion of this parameter will be always ignored.
+ *
+ *
+ *
+ *
+ * @author Milad Naseri (mmnaseri@programmer.net)
+ * @since 1.0 (9/17/15)
+ */
+public class MethodQueryDescriptionExtractor implements QueryDescriptionExtractor {
+
+ private static final String ALL_IGNORE_CASE_SUFFIX = "(AllIgnoreCase|AllIgnoresCase|AllIgnoringCase)$";
+ private static final String IGNORE_CASE_SUFFIX = "(IgnoreCase|IgnoresCase|IgnoringCase)$";
+ private static final String ASC_SUFFIX = "Asc";
+ private static final String DESC_SUFFIX = "Desc";
+ private static final String DEFAULT_OPERATOR_SUFFIX = "Is";
+
+ private final OperatorContext operatorContext;
+
+ public MethodQueryDescriptionExtractor(OperatorContext operatorContext) {
+ this.operatorContext = operatorContext;
+ }
+
+ /**
+ * Extracts query description from a method's name. This will be done according to {@link MethodQueryDescriptionExtractor the parsing rules}
+ * for this extractor.
+ *
+ * @param repositoryMetadata the repository metadata for this method.
+ * @param configuration the repository factory configuration. This will be passed down through the description.
+ * @param method the query method
+ * @return the description for the query
+ */
+ @Override
+ public QueryDescriptor extract(RepositoryMetadata repositoryMetadata, RepositoryFactoryConfiguration configuration, Method method) {
+ String methodName = method.getName();
+ //check to see if the AllIgnoreCase flag is set
+ boolean allIgnoreCase = methodName.matches(".*" + ALL_IGNORE_CASE_SUFFIX);
+ //we need to unify method name afterwards
+ methodName = allIgnoreCase ? methodName.replaceFirst(ALL_IGNORE_CASE_SUFFIX, "") : methodName;
+ //create a document reader for processing method name
+ final DocumentReader reader = new DefaultDocumentReader(methodName);
+ String function = parseFunctionName(method, reader);
+ final QueryModifiers queryModifiers = parseQueryModifiers(method, reader);
+ //this is the extractor used for getting paging data
+ final PageParameterExtractor pageExtractor;
+ //this is the extractor used for getting sorting data
+ SortParameterExtractor sortExtractor = null;
+ //these are decision branches, each of which denoting an AND clause
+ final List> branches = new ArrayList<>();
+ //if the method name simply was the function name, no metadata can be extracted
+ if (!reader.hasMore()) {
+ pageExtractor = null;
+ sortExtractor = null;
+ } else {
+ reader.expect("By");
+ if (!reader.hasMore()) {
+ throw new QueryParserException(method.getDeclaringClass(), "Query method name cannot end with `By`");
+ }
+ //current parameter index
+ int index = parseExpression(repositoryMetadata, method, methodName, allIgnoreCase, reader, branches);
+ final com.mmnaseri.utils.spring.data.query.Sort sort = parseSort(repositoryMetadata, method, reader);
+ pageExtractor = getPageParameterExtractor(method, index, sort);
+ sortExtractor = getSortParameterExtractor(method, index, sort);
+ }
+ return new DefaultQueryDescriptor(queryModifiers.isDistinct(), function, queryModifiers.getLimit(), pageExtractor, sortExtractor, branches, configuration, repositoryMetadata);
+ }
+
+ private SortParameterExtractor getSortParameterExtractor(Method method, int index, com.mmnaseri.utils.spring.data.query.Sort sort) {
+ SortParameterExtractor sortExtractor = null;
+ if (method.getParameterTypes().length == index) {
+ sortExtractor = sort == null ? null : new WrappedSortParameterExtractor(sort);
+ } else if (method.getParameterTypes().length == index + 1) {
+ if (Pageable.class.isAssignableFrom(method.getParameterTypes()[index])) {
+ sortExtractor = sort == null ? new PageableSortParameterExtractor(index) : new WrappedSortParameterExtractor(sort);
+ } else if (Sort.class.isAssignableFrom(method.getParameterTypes()[index])) {
+ sortExtractor = new DirectSortParameterExtractor(index);
+ }
+ }
+ return sortExtractor;
+ }
+
+ private PageParameterExtractor getPageParameterExtractor(Method method, int index, com.mmnaseri.utils.spring.data.query.Sort sort) {
+ PageParameterExtractor pageExtractor;
+ if (method.getParameterTypes().length == index) {
+ pageExtractor = null;
+ } else if (method.getParameterTypes().length == index + 1) {
+ if (Pageable.class.isAssignableFrom(method.getParameterTypes()[index])) {
+ pageExtractor = new PageablePageParameterExtractor(index);
+ } else if (Sort.class.isAssignableFrom(method.getParameterTypes()[index])) {
+ if (sort != null) {
+ throw new QueryParserException(method.getDeclaringClass(), "You cannot specify both an order-by clause and a dynamic ordering");
+ }
+ pageExtractor = null;
+ } else {
+ throw new QueryParserException(method.getDeclaringClass(), "Invalid last argument: expected paging or sorting " + method);
+ }
+ } else {
+ throw new QueryParserException(method.getDeclaringClass(), "Too many parameters declared for query method " + method);
+ }
+ return pageExtractor;
+ }
+
+ private int parseExpression(RepositoryMetadata repositoryMetadata, Method method, String methodName, boolean allIgnoreCase, DocumentReader reader, List> branches) {
+ int index = 0;
+ branches.add(new LinkedList());
+ while (reader.hasMore()) {
+ final Parameter parameter;
+ //read a full expression
+ String expression = parseInitialExpression(reader);
+ //if the expression ended in Or, this is the end of this branch
+ boolean branchEnd = expression.endsWith("Or");
+ //if the expression contains an OrderBy, it is not only the end of the branch, but also the end of the query
+ boolean expressionEnd = expression.matches(".+[a-z]OrderBy[A-Z].+");
+ expression = handleExpressionEnd(reader, expression, expressionEnd);
+ final Set modifiers = new HashSet<>();
+ expression = parseModifiers(allIgnoreCase, expression, modifiers);
+ //if the expression ends in And/Or, we expect there to be more
+ if (expression.matches(".*?(And|Or)$") && !reader.hasMore()) {
+ throw new QueryParserException(method.getDeclaringClass(), "Expected more tokens to follow AND/OR operator");
+ }
+ expression = expression.replaceFirst("(And|Or)$", "");
+ String foundProperty = null;
+ Operator operator = parseOperator(expression);
+ if (operator != null) {
+ foundProperty = expression.substring(0, expression.length() - ((MatchedOperator) operator).getMatchedToken().length());
+ }
+ //if no operator was found, it is the implied "IS" operator
+ if (operator == null || foundProperty.isEmpty()) {
+ foundProperty = expression;
+ operator = operatorContext.getBySuffix(DEFAULT_OPERATOR_SUFFIX);
+ }
+ final PropertyDescriptor propertyDescriptor = getPropertyDescriptor(repositoryMetadata, method, foundProperty);
+ final String property = propertyDescriptor.getPath();
+ //we need to match the method parameters with the operands for the designated operator
+ final int[] indices = new int[operator.getOperands()];
+ index = parseParameterIndices(method, methodName, index, operator, propertyDescriptor, indices);
+ //create a parameter definition for the given expression
+ parameter = new ImmutableParameter(property, modifiers, indices, operator);
+ //get the current branch
+ final List currentBranch = branches.get(branches.size() - 1);
+ //add this parameter to the latest branch
+ currentBranch.add(parameter);
+ //if the branch has ended with "OR", we set up a new branch
+ if (branchEnd) {
+ branches.add(new LinkedList());
+ }
+ //if this is the end of expression, so we need to jump out
+ if (expressionEnd) {
+ break;
+ }
+ }
+ return index;
+ }
+
+ private com.mmnaseri.utils.spring.data.query.Sort parseSort(RepositoryMetadata repositoryMetadata, Method method, DocumentReader reader) {
+ final com.mmnaseri.utils.spring.data.query.Sort sort;
+ //let's figure out if there is a sort requirement embedded in the query definition
+ if (reader.read("OrderBy") != null) {
+ final List orders = new ArrayList<>();
+ while (reader.hasMore()) {
+ orders.add(parseOrder(method, reader, repositoryMetadata));
+ }
+ sort = new ImmutableSort(orders);
+ } else {
+ sort = null;
+ }
+ return sort;
+ }
+
+ private int parseParameterIndices(Method method, String methodName, int index, Operator operator, PropertyDescriptor propertyDescriptor, int[] indices) {
+ int parameterIndex = index;
+ for (int i = 0; i < operator.getOperands(); i++) {
+ if (parameterIndex >= method.getParameterTypes().length) {
+ throw new QueryParserException(method.getDeclaringClass(), "Expected to see parameter with index " + parameterIndex);
+ }
+ if (!propertyDescriptor.getType().isAssignableFrom(method.getParameterTypes()[parameterIndex])) {
+ throw new QueryParserException(method.getDeclaringClass(), "Expected parameter " + parameterIndex + " on method " + methodName + " to be a descendant of " + propertyDescriptor.getType());
+ }
+ indices[i] = parameterIndex ++;
+ }
+ return parameterIndex;
+ }
+
+ private PropertyDescriptor getPropertyDescriptor(RepositoryMetadata repositoryMetadata, Method method, String property) {
+ //let's get the property descriptor
+ final PropertyDescriptor propertyDescriptor;
+ try {
+ propertyDescriptor = PropertyUtils.getPropertyDescriptor(repositoryMetadata.getEntityType(), property);
+ } catch (Exception e) {
+ throw new QueryParserException(method.getDeclaringClass(), "Could not find property `" + StringUtils.uncapitalize(property) + "` on `" + repositoryMetadata.getEntityType() + "`", e);
+ }
+ return propertyDescriptor;
+ }
+
+ private Operator parseOperator(String expression) {
+ Operator operator = null;
+ //let's find out the operator that covers the longest suffix of the operation
+ for (int i = 1; i < expression.length(); i++) {
+ final String suffix = expression.substring(i);
+ operator = operatorContext.getBySuffix(suffix);
+ if (operator != null) {
+ operator = new ImmutableMatchedOperator(operator, suffix);
+ break;
+ }
+ }
+ return operator;
+ }
+
+ private String parseModifiers(boolean allIgnoreCase, String originalExpression, Set modifiers) {
+ String expression = originalExpression;
+ if (expression.matches(".*" + IGNORE_CASE_SUFFIX)) {
+ //if the expression ended in IgnoreCase, we need to strip that off
+ modifiers.add(Modifier.IGNORE_CASE);
+ expression = expression.replaceFirst(IGNORE_CASE_SUFFIX, "");
+ } else if (allIgnoreCase) {
+ //if we had already set "AllIgnoreCase", we will still add the modifier
+ modifiers.add(Modifier.IGNORE_CASE);
+ }
+ return expression;
+ }
+
+ private String handleExpressionEnd(DocumentReader reader, String originalExpression, boolean expressionEnd) {
+ String expression = originalExpression;
+ if (expressionEnd) {
+ //if that is the case, we need to put back the entirety of the order by clause
+ int length = expression.length();
+ expression = expression.replaceFirst("^(.+[a-z])OrderBy[A-Z].+$", "$1");
+ length -= expression.length();
+ reader.backtrack(length);
+ }
+ return expression;
+ }
+
+ private String parseInitialExpression(DocumentReader reader) {
+ String expression = reader.expect("(.*?)(And[A-Z]|Or[A-Z]|$)");
+ if (expression.matches(".*?(And|Or)[A-Z]")) {
+ //if the expression ended in And/Or, we need to put the one extra character we scanned back
+ //we scan one extra character because we don't want anything like "Order" to be mistaken for "Or"
+ reader.backtrack(1);
+ expression = expression.substring(0, expression.length() - 1);
+ }
+ return expression;
+ }
+
+ private Order parseOrder(Method method, DocumentReader reader, RepositoryMetadata repositoryMetadata) {
+ String expression = reader.expect(".*?(Asc|Desc)");
+ final SortDirection direction;
+ if (expression.endsWith(ASC_SUFFIX)) {
+ direction = SortDirection.ASCENDING;
+ expression = expression.substring(0, expression.length() - ASC_SUFFIX.length());
+ } else {
+ direction = SortDirection.DESCENDING;
+ expression = expression.substring(0, expression.length() - DESC_SUFFIX.length());
+ }
+ final PropertyDescriptor propertyDescriptor;
+ try {
+ propertyDescriptor = PropertyUtils.getPropertyDescriptor(repositoryMetadata.getEntityType(), expression);
+ } catch (Exception e) {
+ throw new QueryParserException(method.getDeclaringClass(), "Failed to get a property descriptor for expression: " + expression, e);
+ }
+ if (!Comparable.class.isAssignableFrom(propertyDescriptor.getType())) {
+ throw new QueryParserException(method.getDeclaringClass(), "Sort property `" + propertyDescriptor.getPath() + "` is not comparable in `" + method.getName() + "`");
+ }
+ return new ImmutableOrder(direction, propertyDescriptor.getPath(), NullHandling.DEFAULT);
+ }
+
+ private String parseFunctionName(Method method, DocumentReader reader) {
+ //the first word in the method name is the function name
+ String function = reader.read(Pattern.compile("^[a-z]+"));
+ if (function == null) {
+ throw new QueryParserException(method.getDeclaringClass(), "Malformed query method name: " + method);
+ }
+ //if the method name is one of the following, it is a simple read, and no function is required
+ if (Arrays.asList("read", "find", "query", "get", "load", "select").contains(function)) {
+ function = null;
+ }
+ return function;
+ }
+
+ private QueryModifiers parseQueryModifiers(Method method, DocumentReader reader) {
+ //this is the limit set on the number of items being returned
+ int limit = 0;
+ //this is the flag that determines whether or not the result should be sifted for distinct values
+ boolean distinct = false;
+ //we are still reading the function name if we haven't gotten to `By` and we haven't seen
+ //any of the magic keywords `First`, `Top`, and `Distinct`.
+ //scan for words prior to 'By'
+ while (reader.hasMore() && !reader.has("By")) {
+ //if the next word is Top, then we are setting a limit
+ if (reader.has("First")) {
+ if (limit > 0) {
+ throw new QueryParserException(method.getDeclaringClass(), "There is already a limit of " + limit + " specified for this query: " + method);
+ }
+ reader.expect("First");
+ if (reader.has("\\d+")) {
+ limit = Integer.parseInt(reader.expect("\\d+"));
+ } else {
+ limit = 1;
+ }
+ continue;
+ } else if (reader.has("Top")) {
+ if (limit > 0) {
+ throw new QueryParserException(method.getDeclaringClass(), "There is already a limit of " + limit + " specified for this query: " + method);
+ }
+ reader.expect("Top");
+ limit = Integer.parseInt(reader.expect("\\d+"));
+ continue;
+ } else if (reader.has("Distinct")) {
+ //if the next word is 'Distinct', we are saying we should return distinct results
+ if (distinct) {
+ throw new QueryParserException(method.getDeclaringClass(), "You have already stated that this query should return distinct items: " + method);
+ }
+ distinct = true;
+ }
+ //we read the words until we reach "By".
+ reader.expect("[A-Z][a-z]+");
+ }
+ return new QueryModifiers(limit, distinct);
+ }
+
+ public OperatorContext getOperatorContext() {
+ return operatorContext;
+ }
+
+}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractor.java
index b163ad54..1168cd2f 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractor.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractor.java
@@ -1,398 +1,15 @@
package com.mmnaseri.utils.spring.data.domain.impl;
-import com.mmnaseri.utils.spring.data.domain.*;
-import com.mmnaseri.utils.spring.data.error.QueryParserException;
+import com.mmnaseri.utils.spring.data.domain.RepositoryMetadata;
import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
-import com.mmnaseri.utils.spring.data.query.*;
-import com.mmnaseri.utils.spring.data.query.impl.*;
-import com.mmnaseri.utils.spring.data.string.DocumentReader;
-import com.mmnaseri.utils.spring.data.string.impl.DefaultDocumentReader;
-import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
-import com.mmnaseri.utils.spring.data.tools.StringUtils;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
-
-import java.lang.reflect.Method;
-import java.util.*;
-import java.util.regex.Pattern;
+import com.mmnaseri.utils.spring.data.query.QueryDescriptor;
/**
- *
This class will parse a query method's name and extract a {@link com.mmnaseri.utils.spring.data.dsl.factory.QueryDescription query description}
- * from that name.
- *
- *
In parsing the name, words are considered as being tokens in a camel case name.
- *
- *
Here is how a query method's name is parsed:
- *
- *
- *
We will look at the first word in the name until we reach one of the keywords that says we are specifying a limit, or
- * we are going over the criteria defined by the method. This prefix will be called the "function name" for the operation
- * defined by the query method. If the function name is one of "read", "find", "query", "get", "load", or "select", we will
- * set the function name to {@literal null} to indicate that no special function should be applied to the result set. We
- * are only looking at the first word to let you be more verbose about the purpose of your query (e.g.
- * {@literal findAllGreatPeopleByGreatnessGreaterThan(Integer greatness)} will still resolve to the function
- * {@literal find}, which will ultimately be returned as {@literal null}
- *
We will then look for any of these patterns:
- *
- *
The word {@literal By}, signifying that we are ready to start parsing the query criteria
- *
One of the words {@literal First} or {@literal Top}, signifying that we should look for a limit on the number
- * of results returned.
- *
The word {@literal Distinct}, signifying that the results should include no duplicates.
- *
- *
- *
If the word {@literal First} had appeared, we will see if it is followed by an integer. If it is, that will be the limit.
- * If not, a limit of {@literal 1} is assumed.
- *
If the word {@literal Top} had appeared, we will look for the limit number, which should be an integer value.
- *
At this point, we will continue until we see 'By'. In the above, steps, we will look for the keywords in any order,
- * and there can be any words in between. So, {@literal getTop5StudentsWhoAreAwesomeDistinct} is the same as {@literal getTop5Distinct}
- *
Once we reach the word "By", we will read the query in terms of "decision branches". Branches are separated using the keyword
- * "Or", and each branch is a series of conjunctions. So, while you are separating your conditions with "And", you are in the same branch.
- *
A single branch consists of the pattern: "(Property)(Operator)?((And)(Property)(Operator)?)*". If the operator is missing, "Is" is assumed.
- * Properties must match a proper property in the domain object. So, if you have "AddressZipPrefix" in your query method name, there must be a property
- * reachable by one of the following paths in your domain class (in the given order):
- *
- *
{@literal addressZipPrefix}
- *
{@literal addressZip.prefix}
- *
{@literal address.zipPrefix}
- *
{@literal address.zip.prefix}
- *
- * Note that if you have both the "addressZip" and "address.zip" in your entity, the first will be taken up. To force the parser to choose the former, use
- * the underscore character ({@literal _}) in place of the dot, like so: "{@literal Address_Zip}"
- * Depending on the operator that was matched to the suffix provided (e.g. GreaterThan, Is, etc.), a given number of method parameters will be matched
- * as the operands to that operator. For instance, "Is" requires two values to determine equality, one if the property found on the domain object, and
- * the other must be provided by the query method.
- * The operators themselves are scanned eagerly and based on the set of operators defined in the {@link OperatorContext}.
- *
- *
We continue the pattern indicated above, until we reach the end of the method name, or we reach the "OrderBy" pattern. Once we see "OrderBy"
- * we expect the following pattern: "((Property)(Direction))+", wherein "Property" must follow the same rule as above, and "Direction" is one of
- * "Asc" and "Desc" to indicate "ascending" and "descending" ordering, respectively.
- *
Finally, we look to see if the keyword "AllIgnoreCase" or {@link #ALL_IGNORE_CASE_SUFFIX one of its variations} is present at the end of the
- * query name, which will indicate all applicable comparisons should be case-insensitive.
- *
At the end, we allow one additional parameter for the query method, which can be of either of these types:
- *
- *
{@link Sort Sort}: to indicate a dynamic sort defined at runtime. If a static sort is already indicated via the pattern above, this will
- * result in an error.
- *
{@link Pageable Pageable}: to indicate a paging (and, possibly, sorting) at runtime. If a static sort is already indicated via the pattern
- * above, the sort portion of this parameter will be always ignored.
- *
- *
- *
- *
- * @author Milad Naseri (mmnaseri@programmer.net)
- * @since 1.0 (9/17/15)
+ * @author Milad Naseri (milad.naseri@cdk.com)
+ * @since 1.0 (6/9/16, 12:29 AM)
*/
-public class QueryDescriptionExtractor {
-
- private static final String ALL_IGNORE_CASE_SUFFIX = "(AllIgnoreCase|AllIgnoresCase|AllIgnoringCase)$";
- private static final String IGNORE_CASE_SUFFIX = "(IgnoreCase|IgnoresCase|IgnoringCase)$";
- private static final String ASC_SUFFIX = "Asc";
- private static final String DESC_SUFFIX = "Desc";
- private static final String DEFAULT_OPERATOR_SUFFIX = "Is";
-
- private final OperatorContext operatorContext;
-
- public QueryDescriptionExtractor(OperatorContext operatorContext) {
- this.operatorContext = operatorContext;
- }
-
- /**
- * Extracts query description from a method's name. This will be done according to {@link QueryDescriptionExtractor the parsing rules}
- * for this extractor.
- *
- * @param repositoryMetadata the repository metadata for this method.
- * @param method the query method
- * @param configuration the repository factory configuration. This will be passed down through the description.
- * @return the description for the query
- */
- public QueryDescriptor extract(RepositoryMetadata repositoryMetadata, Method method, RepositoryFactoryConfiguration configuration) {
- String methodName = method.getName();
- //check to see if the AllIgnoreCase flag is set
- boolean allIgnoreCase = methodName.matches(".*" + ALL_IGNORE_CASE_SUFFIX);
- //we need to unify method name afterwards
- methodName = allIgnoreCase ? methodName.replaceFirst(ALL_IGNORE_CASE_SUFFIX, "") : methodName;
- //create a document reader for processing method name
- final DocumentReader reader = new DefaultDocumentReader(methodName);
- String function = parseFunctionName(method, reader);
- final QueryModifiers queryModifiers = parseQueryModifiers(method, reader);
- //this is the extractor used for getting paging data
- final PageParameterExtractor pageExtractor;
- //this is the extractor used for getting sorting data
- SortParameterExtractor sortExtractor = null;
- //these are decision branches, each of which denoting an AND clause
- final List> branches = new ArrayList<>();
- //if the method name simply was the function name, no metadata can be extracted
- if (!reader.hasMore()) {
- pageExtractor = null;
- sortExtractor = null;
- } else {
- reader.expect("By");
- if (!reader.hasMore()) {
- throw new QueryParserException(method.getDeclaringClass(), "Query method name cannot end with `By`");
- }
- //current parameter index
- int index = parseExpression(repositoryMetadata, method, methodName, allIgnoreCase, reader, branches);
- final com.mmnaseri.utils.spring.data.query.Sort sort = parseSort(repositoryMetadata, method, reader);
- pageExtractor = getPageParameterExtractor(method, index, sort);
- sortExtractor = getSortParameterExtractor(method, index, sort);
- }
- return new DefaultQueryDescriptor(queryModifiers.isDistinct(), function, queryModifiers.getLimit(), pageExtractor, sortExtractor, branches, configuration, repositoryMetadata);
- }
-
- private SortParameterExtractor getSortParameterExtractor(Method method, int index, com.mmnaseri.utils.spring.data.query.Sort sort) {
- SortParameterExtractor sortExtractor = null;
- if (method.getParameterTypes().length == index) {
- sortExtractor = sort == null ? null : new WrappedSortParameterExtractor(sort);
- } else if (method.getParameterTypes().length == index + 1) {
- if (Pageable.class.isAssignableFrom(method.getParameterTypes()[index])) {
- sortExtractor = sort == null ? new PageableSortParameterExtractor(index) : new WrappedSortParameterExtractor(sort);
- } else if (Sort.class.isAssignableFrom(method.getParameterTypes()[index])) {
- sortExtractor = new DirectSortParameterExtractor(index);
- }
- }
- return sortExtractor;
- }
-
- private PageParameterExtractor getPageParameterExtractor(Method method, int index, com.mmnaseri.utils.spring.data.query.Sort sort) {
- PageParameterExtractor pageExtractor;
- if (method.getParameterTypes().length == index) {
- pageExtractor = null;
- } else if (method.getParameterTypes().length == index + 1) {
- if (Pageable.class.isAssignableFrom(method.getParameterTypes()[index])) {
- pageExtractor = new PageablePageParameterExtractor(index);
- } else if (Sort.class.isAssignableFrom(method.getParameterTypes()[index])) {
- if (sort != null) {
- throw new QueryParserException(method.getDeclaringClass(), "You cannot specify both an order-by clause and a dynamic ordering");
- }
- pageExtractor = null;
- } else {
- throw new QueryParserException(method.getDeclaringClass(), "Invalid last argument: expected paging or sorting " + method);
- }
- } else {
- throw new QueryParserException(method.getDeclaringClass(), "Too many parameters declared for query method " + method);
- }
- return pageExtractor;
- }
-
- private int parseExpression(RepositoryMetadata repositoryMetadata, Method method, String methodName, boolean allIgnoreCase, DocumentReader reader, List> branches) {
- int index = 0;
- branches.add(new LinkedList());
- while (reader.hasMore()) {
- final Parameter parameter;
- //read a full expression
- String expression = parseInitialExpression(reader);
- //if the expression ended in Or, this is the end of this branch
- boolean branchEnd = expression.endsWith("Or");
- //if the expression contains an OrderBy, it is not only the end of the branch, but also the end of the query
- boolean expressionEnd = expression.matches(".+[a-z]OrderBy[A-Z].+");
- expression = handleExpressionEnd(reader, expression, expressionEnd);
- final Set modifiers = new HashSet<>();
- expression = parseModifiers(allIgnoreCase, expression, modifiers);
- //if the expression ends in And/Or, we expect there to be more
- if (expression.matches(".*?(And|Or)$") && !reader.hasMore()) {
- throw new QueryParserException(method.getDeclaringClass(), "Expected more tokens to follow AND/OR operator");
- }
- expression = expression.replaceFirst("(And|Or)$", "");
- String foundProperty = null;
- Operator operator = parseOperator(expression);
- if (operator != null) {
- foundProperty = expression.substring(0, expression.length() - ((MatchedOperator) operator).getMatchedToken().length());
- }
- //if no operator was found, it is the implied "IS" operator
- if (operator == null || foundProperty.isEmpty()) {
- foundProperty = expression;
- operator = operatorContext.getBySuffix(DEFAULT_OPERATOR_SUFFIX);
- }
- final PropertyDescriptor propertyDescriptor = getPropertyDescriptor(repositoryMetadata, method, foundProperty);
- final String property = propertyDescriptor.getPath();
- //we need to match the method parameters with the operands for the designated operator
- final int[] indices = new int[operator.getOperands()];
- index = parseParameterIndices(method, methodName, index, operator, propertyDescriptor, indices);
- //create a parameter definition for the given expression
- parameter = new ImmutableParameter(property, modifiers, indices, operator);
- //get the current branch
- final List currentBranch = branches.get(branches.size() - 1);
- //add this parameter to the latest branch
- currentBranch.add(parameter);
- //if the branch has ended with "OR", we set up a new branch
- if (branchEnd) {
- branches.add(new LinkedList());
- }
- //if this is the end of expression, so we need to jump out
- if (expressionEnd) {
- break;
- }
- }
- return index;
- }
-
- private com.mmnaseri.utils.spring.data.query.Sort parseSort(RepositoryMetadata repositoryMetadata, Method method, DocumentReader reader) {
- final com.mmnaseri.utils.spring.data.query.Sort sort;
- //let's figure out if there is a sort requirement embedded in the query definition
- if (reader.read("OrderBy") != null) {
- final List orders = new ArrayList<>();
- while (reader.hasMore()) {
- orders.add(parseOrder(method, reader, repositoryMetadata));
- }
- sort = new ImmutableSort(orders);
- } else {
- sort = null;
- }
- return sort;
- }
-
- private int parseParameterIndices(Method method, String methodName, int index, Operator operator, PropertyDescriptor propertyDescriptor, int[] indices) {
- int parameterIndex = index;
- for (int i = 0; i < operator.getOperands(); i++) {
- if (parameterIndex >= method.getParameterTypes().length) {
- throw new QueryParserException(method.getDeclaringClass(), "Expected to see parameter with index " + parameterIndex);
- }
- if (!propertyDescriptor.getType().isAssignableFrom(method.getParameterTypes()[parameterIndex])) {
- throw new QueryParserException(method.getDeclaringClass(), "Expected parameter " + parameterIndex + " on method " + methodName + " to be a descendant of " + propertyDescriptor.getType());
- }
- indices[i] = parameterIndex ++;
- }
- return parameterIndex;
- }
-
- private PropertyDescriptor getPropertyDescriptor(RepositoryMetadata repositoryMetadata, Method method, String property) {
- //let's get the property descriptor
- final PropertyDescriptor propertyDescriptor;
- try {
- propertyDescriptor = PropertyUtils.getPropertyDescriptor(repositoryMetadata.getEntityType(), property);
- } catch (Exception e) {
- throw new QueryParserException(method.getDeclaringClass(), "Could not find property `" + StringUtils.uncapitalize(property) + "` on `" + repositoryMetadata.getEntityType() + "`", e);
- }
- return propertyDescriptor;
- }
-
- private Operator parseOperator(String expression) {
- Operator operator = null;
- //let's find out the operator that covers the longest suffix of the operation
- for (int i = 1; i < expression.length(); i++) {
- final String suffix = expression.substring(i);
- operator = operatorContext.getBySuffix(suffix);
- if (operator != null) {
- operator = new ImmutableMatchedOperator(operator, suffix);
- break;
- }
- }
- return operator;
- }
-
- private String parseModifiers(boolean allIgnoreCase, String originalExpression, Set modifiers) {
- String expression = originalExpression;
- if (expression.matches(".*" + IGNORE_CASE_SUFFIX)) {
- //if the expression ended in IgnoreCase, we need to strip that off
- modifiers.add(Modifier.IGNORE_CASE);
- expression = expression.replaceFirst(IGNORE_CASE_SUFFIX, "");
- } else if (allIgnoreCase) {
- //if we had already set "AllIgnoreCase", we will still add the modifier
- modifiers.add(Modifier.IGNORE_CASE);
- }
- return expression;
- }
-
- private String handleExpressionEnd(DocumentReader reader, String originalExpression, boolean expressionEnd) {
- String expression = originalExpression;
- if (expressionEnd) {
- //if that is the case, we need to put back the entirety of the order by clause
- int length = expression.length();
- expression = expression.replaceFirst("^(.+[a-z])OrderBy[A-Z].+$", "$1");
- length -= expression.length();
- reader.backtrack(length);
- }
- return expression;
- }
-
- private String parseInitialExpression(DocumentReader reader) {
- String expression = reader.expect("(.*?)(And[A-Z]|Or[A-Z]|$)");
- if (expression.matches(".*?(And|Or)[A-Z]")) {
- //if the expression ended in And/Or, we need to put the one extra character we scanned back
- //we scan one extra character because we don't want anything like "Order" to be mistaken for "Or"
- reader.backtrack(1);
- expression = expression.substring(0, expression.length() - 1);
- }
- return expression;
- }
-
- private Order parseOrder(Method method, DocumentReader reader, RepositoryMetadata repositoryMetadata) {
- String expression = reader.expect(".*?(Asc|Desc)");
- final SortDirection direction;
- if (expression.endsWith(ASC_SUFFIX)) {
- direction = SortDirection.ASCENDING;
- expression = expression.substring(0, expression.length() - ASC_SUFFIX.length());
- } else {
- direction = SortDirection.DESCENDING;
- expression = expression.substring(0, expression.length() - DESC_SUFFIX.length());
- }
- final PropertyDescriptor propertyDescriptor;
- try {
- propertyDescriptor = PropertyUtils.getPropertyDescriptor(repositoryMetadata.getEntityType(), expression);
- } catch (Exception e) {
- throw new QueryParserException(method.getDeclaringClass(), "Failed to get a property descriptor for expression: " + expression, e);
- }
- if (!Comparable.class.isAssignableFrom(propertyDescriptor.getType())) {
- throw new QueryParserException(method.getDeclaringClass(), "Sort property `" + propertyDescriptor.getPath() + "` is not comparable in `" + method.getName() + "`");
- }
- return new ImmutableOrder(direction, propertyDescriptor.getPath(), NullHandling.DEFAULT);
- }
-
- private String parseFunctionName(Method method, DocumentReader reader) {
- //the first word in the method name is the function name
- String function = reader.read(Pattern.compile("^[a-z]+"));
- if (function == null) {
- throw new QueryParserException(method.getDeclaringClass(), "Malformed query method name: " + method);
- }
- //if the method name is one of the following, it is a simple read, and no function is required
- if (Arrays.asList("read", "find", "query", "get", "load", "select").contains(function)) {
- function = null;
- }
- return function;
- }
-
- private QueryModifiers parseQueryModifiers(Method method, DocumentReader reader) {
- //this is the limit set on the number of items being returned
- int limit = 0;
- //this is the flag that determines whether or not the result should be sifted for distinct values
- boolean distinct = false;
- //we are still reading the function name if we haven't gotten to `By` and we haven't seen
- //any of the magic keywords `First`, `Top`, and `Distinct`.
- //scan for words prior to 'By'
- while (reader.hasMore() && !reader.has("By")) {
- //if the next word is Top, then we are setting a limit
- if (reader.has("First")) {
- if (limit > 0) {
- throw new QueryParserException(method.getDeclaringClass(), "There is already a limit of " + limit + " specified for this query: " + method);
- }
- reader.expect("First");
- if (reader.has("\\d+")) {
- limit = Integer.parseInt(reader.expect("\\d+"));
- } else {
- limit = 1;
- }
- continue;
- } else if (reader.has("Top")) {
- if (limit > 0) {
- throw new QueryParserException(method.getDeclaringClass(), "There is already a limit of " + limit + " specified for this query: " + method);
- }
- reader.expect("Top");
- limit = Integer.parseInt(reader.expect("\\d+"));
- continue;
- } else if (reader.has("Distinct")) {
- //if the next word is 'Distinct', we are saying we should return distinct results
- if (distinct) {
- throw new QueryParserException(method.getDeclaringClass(), "You have already stated that this query should return distinct items: " + method);
- }
- distinct = true;
- }
- //we read the words until we reach "By".
- reader.expect("[A-Z][a-z]+");
- }
- return new QueryModifiers(limit, distinct);
- }
+public interface QueryDescriptionExtractor {
- public OperatorContext getOperatorContext() {
- return operatorContext;
- }
+ QueryDescriptor extract(RepositoryMetadata repositoryMetadata, RepositoryFactoryConfiguration configuration, T target);
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolver.java
index d4a53f27..a7ae7599 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolver.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolver.java
@@ -10,6 +10,8 @@
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicReference;
+import static com.mmnaseri.utils.spring.data.domain.impl.id.IdPropertyResolverUtils.isAnnotated;
+
/**
* This class will help resolve ID property name if the entity has a field that is annotated with
* {@link Id @Id}
@@ -27,11 +29,11 @@ public String resolve(final Class> entityType, Class extends Serializable> i
ReflectionUtils.doWithFields(entityType, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
- if (field.isAnnotationPresent(Id.class)) {
+ if (isAnnotated(field)) {
if (found.get() == null) {
found.set(field);
} else {
- throw new MultipleIdPropertiesException(entityType, Id.class);
+ throw new MultipleIdPropertiesException(entityType);
}
}
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolver.java
index 305a2ca2..b471783f 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolver.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolver.java
@@ -1,8 +1,8 @@
package com.mmnaseri.utils.spring.data.domain.impl.id;
+import com.mmnaseri.utils.spring.data.domain.IdPropertyResolver;
import com.mmnaseri.utils.spring.data.error.MultipleIdPropertiesException;
import com.mmnaseri.utils.spring.data.tools.GetterMethodFilter;
-import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.Id;
import org.springframework.util.ReflectionUtils;
@@ -10,6 +10,9 @@
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicReference;
+import static com.mmnaseri.utils.spring.data.domain.impl.id.IdPropertyResolverUtils.getPropertyNameFromAnnotatedMethod;
+import static com.mmnaseri.utils.spring.data.domain.impl.id.IdPropertyResolverUtils.isAnnotated;
+
/**
* This class will resolve ID property name from a getter method that is annotated with
* {@link Id @Id}.
@@ -18,7 +21,7 @@
* @since 1.0 (9/23/15)
*/
@SuppressWarnings("WeakerAccess")
-public class AnnotatedGetterIdPropertyResolver extends AnnotatedIdPropertyResolver {
+public class AnnotatedGetterIdPropertyResolver implements IdPropertyResolver {
@Override
public String resolve(final Class> entityType, Class extends Serializable> idType) {
@@ -26,11 +29,11 @@ public String resolve(final Class> entityType, Class extends Serializable> i
ReflectionUtils.doWithMethods(entityType, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
- if (AnnotationUtils.findAnnotation(method, Id.class) != null) {
+ if (isAnnotated(method)) {
if (found.get() == null) {
found.set(method);
} else {
- throw new MultipleIdPropertiesException(entityType, Id.class);
+ throw new MultipleIdPropertiesException(entityType);
}
}
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedIdPropertyResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedIdPropertyResolver.java
deleted file mode 100644
index abf0b920..00000000
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedIdPropertyResolver.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.mmnaseri.utils.spring.data.domain.impl.id;
-
-import com.mmnaseri.utils.spring.data.domain.IdPropertyResolver;
-import com.mmnaseri.utils.spring.data.error.PropertyTypeMismatchException;
-import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
-
-import java.io.Serializable;
-import java.lang.reflect.Method;
-
-/**
- * This class and its children will help resolve the ID property from an annotated element
- *
- * @author Milad Naseri (mmnaseri@programmer.net)
- * @since 1.0 (4/8/16)
- */
-@SuppressWarnings("WeakerAccess")
-abstract class AnnotatedIdPropertyResolver implements IdPropertyResolver {
-
- /**
- * Returns the name of the property as represented by the method given
- * @param entityType the type of the entity that the ID is being resolved for
- * @param idType the type of the ID expected for the entity
- * @param idAnnotatedMethod the method that will return the ID (e.g. getter for the ID property)
- * @return the name of the property, or {@literal null} if the method is {@literal null}
- */
- protected String getPropertyNameFromAnnotatedMethod(Class> entityType, Class extends Serializable> idType, Method idAnnotatedMethod) {
- if (idAnnotatedMethod != null) {
- final String name = PropertyUtils.getPropertyName(idAnnotatedMethod);
- if (!idType.isAssignableFrom(idAnnotatedMethod.getReturnType())) {
- throw new PropertyTypeMismatchException(entityType, name, idType, idAnnotatedMethod.getReturnType());
- } else {
- return name;
- }
- }
- return null;
- }
-}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java
new file mode 100644
index 00000000..020e9e82
--- /dev/null
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java
@@ -0,0 +1,91 @@
+package com.mmnaseri.utils.spring.data.domain.impl.id;
+
+import com.mmnaseri.utils.spring.data.error.PropertyTypeMismatchException;
+import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.util.ClassUtils;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Mohammad Milad Naseri (m.m.naseri@gmail.com)
+ * @since 1.0 (6/8/16, 1:43 AM)
+ */
+final class IdPropertyResolverUtils {
+
+ private static final List ID_ANNOTATIONS = new ArrayList<>();
+ private static final Log log = LogFactory.getLog(IdPropertyResolverUtils.class);
+
+ static {
+ ID_ANNOTATIONS.add("org.springframework.data.annotation.Id");
+ ID_ANNOTATIONS.add("javax.persistence.Id");
+ }
+
+ private IdPropertyResolverUtils() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the name of the property as represented by the method given
+ * @param entityType the type of the entity that the ID is being resolved for
+ * @param idType the type of the ID expected for the entity
+ * @param idAnnotatedMethod the method that will return the ID (e.g. getter for the ID property)
+ * @return the name of the property, or {@literal null} if the method is {@literal null}
+ */
+ static String getPropertyNameFromAnnotatedMethod(Class> entityType, Class extends Serializable> idType, Method idAnnotatedMethod) {
+ if (idAnnotatedMethod != null) {
+ final String name = PropertyUtils.getPropertyName(idAnnotatedMethod);
+ if (!idType.isAssignableFrom(idAnnotatedMethod.getReturnType())) {
+ throw new PropertyTypeMismatchException(entityType, name, idType, idAnnotatedMethod.getReturnType());
+ } else {
+ return name;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Determines whether or not the given element is annotated with the annotations specified by
+ * {@link #getIdAnnotations()}
+ * @param element the element to be examined
+ * @return {@literal true} if the element has any of the ID annotations
+ */
+ static boolean isAnnotated(AnnotatedElement element) {
+ final List> annotations = getIdAnnotations();
+ for (Class extends Annotation> annotation : annotations) {
+ if (AnnotationUtils.findAnnotation(element, annotation) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Lists all the annotations that can be used to mark a property as the ID property
+ * based on the libraries that can be found in the classpath
+ * @return the list of annotations
+ */
+ private static List> getIdAnnotations() {
+ final List> annotations = new ArrayList<>();
+ final ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
+ for (String idAnnotation : ID_ANNOTATIONS) {
+ try {
+ final Class> type = ClassUtils.forName(idAnnotation, classLoader);
+ final Class extends Annotation> annotationType = type.asSubclass(Annotation.class);
+ annotations.add(annotationType);
+ } catch (ClassNotFoundException ignored) {
+ //if the class for the annotation wasn't found, we just ignore it
+ log.debug("Requested ID annotation type " + idAnnotation + " is not present in the classpath");
+ }
+ }
+ return annotations;
+ }
+
+}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/NamedGetterIdPropertyResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/NamedGetterIdPropertyResolver.java
index cf129f4b..194d962c 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/NamedGetterIdPropertyResolver.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/NamedGetterIdPropertyResolver.java
@@ -1,5 +1,6 @@
package com.mmnaseri.utils.spring.data.domain.impl.id;
+import com.mmnaseri.utils.spring.data.domain.IdPropertyResolver;
import com.mmnaseri.utils.spring.data.tools.GetterMethodFilter;
import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
import org.springframework.util.ReflectionUtils;
@@ -8,6 +9,8 @@
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicReference;
+import static com.mmnaseri.utils.spring.data.domain.impl.id.IdPropertyResolverUtils.getPropertyNameFromAnnotatedMethod;
+
/**
* This class is for resolving an ID based on the getter. It will try to find a getter for a property named
* {@literal "id"} -- i.e., it will look for a getter named "getId".
@@ -16,7 +19,7 @@
* @since 1.0 (9/23/15)
*/
@SuppressWarnings("WeakerAccess")
-public class NamedGetterIdPropertyResolver extends AnnotatedIdPropertyResolver {
+public class NamedGetterIdPropertyResolver implements IdPropertyResolver {
@Override
public String resolve(Class> entityType, final Class extends Serializable> idType) {
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/QueryDescription.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/QueryDescription.java
index 8e46044f..70f430b6 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/QueryDescription.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/QueryDescription.java
@@ -1,6 +1,6 @@
package com.mmnaseri.utils.spring.data.dsl.factory;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
/**
* This interface lets us define the query description extractor
@@ -15,6 +15,6 @@ public interface QueryDescription extends DataFunctions {
* @param extractor the extractor
* @return the rest of the configuration
*/
- DataFunctions extractQueriesUsing(QueryDescriptionExtractor extractor);
+ DataFunctions extractQueriesUsing(MethodQueryDescriptionExtractor extractor);
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilder.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilder.java
index 02406601..0345f828 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilder.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilder.java
@@ -6,7 +6,7 @@
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadataResolver;
import com.mmnaseri.utils.spring.data.domain.impl.DefaultOperatorContext;
import com.mmnaseri.utils.spring.data.domain.impl.DefaultRepositoryMetadataResolver;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.dsl.mock.Implementation;
import com.mmnaseri.utils.spring.data.dsl.mock.ImplementationAnd;
import com.mmnaseri.utils.spring.data.dsl.mock.RepositoryMockBuilder;
@@ -36,7 +36,7 @@ public class RepositoryFactoryBuilder implements Start, DataFunctionsAnd, DataSt
private static RepositoryFactoryConfiguration DEFAULT_FACTORY_CONFIGURATION;
public static final String DEFAULT_USER = "User";
private RepositoryMetadataResolver metadataResolver;
- private QueryDescriptionExtractor queryDescriptionExtractor;
+ private MethodQueryDescriptionExtractor queryDescriptionExtractor;
private DataFunctionRegistry functionRegistry;
private DataStoreRegistry dataStoreRegistry;
private ResultAdapterContext resultAdapterContext;
@@ -85,7 +85,7 @@ public static RepositoryFactory defaultFactory() {
private RepositoryFactoryBuilder() {
metadataResolver = new DefaultRepositoryMetadataResolver();
- queryDescriptionExtractor = new QueryDescriptionExtractor(new DefaultOperatorContext());
+ queryDescriptionExtractor = new MethodQueryDescriptionExtractor(new DefaultOperatorContext());
functionRegistry = new DefaultDataFunctionRegistry();
dataStoreRegistry = new DefaultDataStoreRegistry();
resultAdapterContext = new DefaultResultAdapterContext();
@@ -102,7 +102,7 @@ public QueryDescriptionConfigurer resolveMetadataUsing(RepositoryMetadataResolve
@Override
public DataFunctions withOperators(OperatorContext context) {
- queryDescriptionExtractor = new QueryDescriptionExtractor(context);
+ queryDescriptionExtractor = new MethodQueryDescriptionExtractor(context);
return this;
}
@@ -113,7 +113,7 @@ public OperatorsAnd registerOperator(Operator operator) {
}
@Override
- public DataFunctions extractQueriesUsing(QueryDescriptionExtractor extractor) {
+ public DataFunctions extractQueriesUsing(MethodQueryDescriptionExtractor extractor) {
queryDescriptionExtractor = extractor;
return this;
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/error/MultipleIdPropertiesException.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/error/MultipleIdPropertiesException.java
index 0f6375ea..10f78da3 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/error/MultipleIdPropertiesException.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/error/MultipleIdPropertiesException.java
@@ -1,15 +1,13 @@
package com.mmnaseri.utils.spring.data.error;
-import java.lang.annotation.Annotation;
-
/**
* @author Milad Naseri (mmnaseri@programmer.net)
* @since 1.0 (4/8/16)
*/
public class MultipleIdPropertiesException extends EntityDefinitionException {
- public MultipleIdPropertiesException(Class> entityType, Class extends Annotation> annotationType) {
- super("There are multiple properties in " + entityType + " that are annotated with @" + annotationType.getSimpleName());
+ public MultipleIdPropertiesException(Class> entityType) {
+ super("There are multiple properties in " + entityType + " that are annotated as the ID property");
}
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/RepositoryFactoryConfiguration.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/RepositoryFactoryConfiguration.java
index ebdc7fb6..0b54309d 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/RepositoryFactoryConfiguration.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/RepositoryFactoryConfiguration.java
@@ -1,7 +1,7 @@
package com.mmnaseri.utils.spring.data.proxy;
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadataResolver;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.proxy.impl.NonDataOperationInvocationHandler;
import com.mmnaseri.utils.spring.data.query.DataFunctionRegistry;
import com.mmnaseri.utils.spring.data.store.DataStoreEventListenerContext;
@@ -23,7 +23,7 @@ public interface RepositoryFactoryConfiguration {
/**
* @return the description extractor used to extract query metadata from a query method
*/
- QueryDescriptionExtractor getDescriptionExtractor();
+ MethodQueryDescriptionExtractor getDescriptionExtractor();
/**
* @return the function registry containing all the functions used when executing the queries
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactory.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactory.java
index 89e58fbe..3d36bf30 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactory.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactory.java
@@ -1,7 +1,7 @@
package com.mmnaseri.utils.spring.data.proxy.impl;
import com.mmnaseri.utils.spring.data.domain.*;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.proxy.*;
import com.mmnaseri.utils.spring.data.proxy.impl.resolvers.DefaultDataOperationResolver;
import com.mmnaseri.utils.spring.data.query.DataFunctionRegistry;
@@ -36,7 +36,7 @@ public class DefaultRepositoryFactory implements RepositoryFactory {
private static final Log log = LogFactory.getLog(DefaultRepositoryFactory.class);
private final RepositoryMetadataResolver repositoryMetadataResolver;
private final Map, RepositoryMetadata> metadataMap = new ConcurrentHashMap<>();
- private final QueryDescriptionExtractor descriptionExtractor;
+ private final MethodQueryDescriptionExtractor descriptionExtractor;
private final DataFunctionRegistry functionRegistry;
private final DataStoreRegistry dataStoreRegistry;
private final ResultAdapterContext adapterContext;
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryConfiguration.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryConfiguration.java
index 77aa55a6..d75c6321 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryConfiguration.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryConfiguration.java
@@ -1,7 +1,7 @@
package com.mmnaseri.utils.spring.data.proxy.impl;
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadataResolver;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
import com.mmnaseri.utils.spring.data.proxy.ResultAdapterContext;
import com.mmnaseri.utils.spring.data.proxy.TypeMappingContext;
@@ -18,7 +18,7 @@
public class DefaultRepositoryFactoryConfiguration implements RepositoryFactoryConfiguration {
private RepositoryMetadataResolver repositoryMetadataResolver;
- private QueryDescriptionExtractor descriptionExtractor;
+ private MethodQueryDescriptionExtractor descriptionExtractor;
private DataFunctionRegistry functionRegistry;
private DataStoreRegistry dataStoreRegistry;
private ResultAdapterContext resultAdapterContext;
@@ -36,11 +36,11 @@ public void setRepositoryMetadataResolver(RepositoryMetadataResolver repositoryM
}
@Override
- public QueryDescriptionExtractor getDescriptionExtractor() {
+ public MethodQueryDescriptionExtractor getDescriptionExtractor() {
return descriptionExtractor;
}
- public void setDescriptionExtractor(QueryDescriptionExtractor descriptionExtractor) {
+ public void setDescriptionExtractor(MethodQueryDescriptionExtractor descriptionExtractor) {
this.descriptionExtractor = descriptionExtractor;
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContext.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContext.java
index 56e8961d..61bc4e21 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContext.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContext.java
@@ -3,10 +3,7 @@
import com.mmnaseri.utils.spring.data.error.RepositoryDefinitionException;
import com.mmnaseri.utils.spring.data.proxy.TypeMapping;
import com.mmnaseri.utils.spring.data.proxy.TypeMappingContext;
-import com.mmnaseri.utils.spring.data.repository.DefaultCrudRepository;
-import com.mmnaseri.utils.spring.data.repository.DefaultGemfireRepository;
-import com.mmnaseri.utils.spring.data.repository.DefaultJpaRepository;
-import com.mmnaseri.utils.spring.data.repository.DefaultPagingAndSortingRepository;
+import com.mmnaseri.utils.spring.data.repository.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -48,14 +45,23 @@ public DefaultTypeMappingContext(boolean registerDefaults) {
this(null);
if (registerDefaults) {
log.info("Trying to register all the default type mappings");
- if (ClassUtils.isPresent("org.springframework.data.gemfire.repository.GemfireRepository", ClassUtils.getDefaultClassLoader())) {
+ final ClassLoader defaultClassLoader = ClassUtils.getDefaultClassLoader();
+ if (ClassUtils.isPresent("org.springframework.data.gemfire.repository.GemfireRepository", defaultClassLoader)) {
log.debug("We seem to have Gemfire in the classpath, so, we should register the supporting registry");
register(Object.class, DefaultGemfireRepository.class);
}
- if (ClassUtils.isPresent("org.springframework.data.jpa.repository.JpaRepository", ClassUtils.getDefaultClassLoader())) {
+ if (ClassUtils.isPresent("org.springframework.data.jpa.repository.JpaRepository", defaultClassLoader)) {
log.debug("JPA support is enabled in this project, so we need to support the methods");
register(Object.class, DefaultJpaRepository.class);
}
+ if (ClassUtils.isPresent("org.springframework.data.querydsl.QueryDslPredicateExecutor", defaultClassLoader)) {
+ log.debug("QueryDSL support is enabled. We will add the proper method implementations.");
+ register(Object.class, DefaultQueryDslPredicateExecutor.class);
+ }
+ if (ClassUtils.isPresent("org.springframework.data.repository.query.QueryByExampleExecutor", defaultClassLoader)) {
+ log.debug("Query by example is enabled. We will the proper method implementations.");
+ register(Object.class, DefaultQueryByExampleExecutor.class);
+ }
register(Object.class, DefaultPagingAndSortingRepository.class);
register(Object.class, DefaultCrudRepository.class);
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfiguration.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfiguration.java
index e57b4443..2d723b2b 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfiguration.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfiguration.java
@@ -1,7 +1,7 @@
package com.mmnaseri.utils.spring.data.proxy.impl;
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadataResolver;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
import com.mmnaseri.utils.spring.data.proxy.ResultAdapterContext;
import com.mmnaseri.utils.spring.data.proxy.TypeMappingContext;
@@ -20,7 +20,7 @@
public class ImmutableRepositoryFactoryConfiguration implements RepositoryFactoryConfiguration {
private final RepositoryMetadataResolver metadataResolver;
- private final QueryDescriptionExtractor queryDescriptionExtractor;
+ private final MethodQueryDescriptionExtractor queryDescriptionExtractor;
private final DataFunctionRegistry functionRegistry;
private final DataStoreRegistry dataStoreRegistry;
private final ResultAdapterContext resultAdapterContext;
@@ -34,7 +34,7 @@ public ImmutableRepositoryFactoryConfiguration(RepositoryFactoryConfiguration co
configuration.getEventListenerContext(), configuration.getOperationInvocationHandler());
}
- public ImmutableRepositoryFactoryConfiguration(RepositoryMetadataResolver metadataResolver, QueryDescriptionExtractor queryDescriptionExtractor, DataFunctionRegistry functionRegistry, DataStoreRegistry dataStoreRegistry, ResultAdapterContext resultAdapterContext, TypeMappingContext typeMappingContext, DataStoreEventListenerContext eventListenerContext, NonDataOperationInvocationHandler operationInvocationHandler) {
+ public ImmutableRepositoryFactoryConfiguration(RepositoryMetadataResolver metadataResolver, MethodQueryDescriptionExtractor queryDescriptionExtractor, DataFunctionRegistry functionRegistry, DataStoreRegistry dataStoreRegistry, ResultAdapterContext resultAdapterContext, TypeMappingContext typeMappingContext, DataStoreEventListenerContext eventListenerContext, NonDataOperationInvocationHandler operationInvocationHandler) {
this.metadataResolver = metadataResolver;
this.queryDescriptionExtractor = queryDescriptionExtractor;
this.functionRegistry = functionRegistry;
@@ -51,7 +51,7 @@ public RepositoryMetadataResolver getRepositoryMetadataResolver() {
}
@Override
- public QueryDescriptionExtractor getDescriptionExtractor() {
+ public MethodQueryDescriptionExtractor getDescriptionExtractor() {
return queryDescriptionExtractor;
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolver.java
index 664e72f5..feb3c96c 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolver.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolver.java
@@ -1,7 +1,7 @@
package com.mmnaseri.utils.spring.data.proxy.impl.resolvers;
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadata;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.error.DataOperationDefinitionException;
import com.mmnaseri.utils.spring.data.error.UnknownDataOperationException;
import com.mmnaseri.utils.spring.data.proxy.DataOperationResolver;
@@ -27,7 +27,7 @@ public class DefaultDataOperationResolver implements DataOperationResolver {
private static final Log log = LogFactory.getLog(DefaultDataOperationResolver.class);
private final List resolvers;
- public DefaultDataOperationResolver(List> implementations, QueryDescriptionExtractor descriptionExtractor, RepositoryMetadata repositoryMetadata, DataFunctionRegistry functionRegistry, RepositoryFactoryConfiguration configuration) {
+ public DefaultDataOperationResolver(List> implementations, MethodQueryDescriptionExtractor descriptionExtractor, RepositoryMetadata repositoryMetadata, DataFunctionRegistry functionRegistry, RepositoryFactoryConfiguration configuration) {
resolvers = new ArrayList<>();
resolvers.add(new SignatureDataOperationResolver(implementations));
resolvers.add(new QueryMethodDataOperationResolver(descriptionExtractor, repositoryMetadata, functionRegistry, configuration));
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolver.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolver.java
index f2041a4b..b22bf4b6 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolver.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolver.java
@@ -2,7 +2,7 @@
import com.mmnaseri.utils.spring.data.domain.RepositoryMetadata;
import com.mmnaseri.utils.spring.data.domain.impl.DescribedDataStoreOperation;
-import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor;
+import com.mmnaseri.utils.spring.data.domain.impl.MethodQueryDescriptionExtractor;
import com.mmnaseri.utils.spring.data.domain.impl.SelectDataStoreOperation;
import com.mmnaseri.utils.spring.data.proxy.DataOperationResolver;
import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
@@ -30,12 +30,12 @@
public class QueryMethodDataOperationResolver implements DataOperationResolver {
private static final Log log = LogFactory.getLog(QueryMethodDataOperationResolver.class);
- private final QueryDescriptionExtractor descriptionExtractor;
+ private final MethodQueryDescriptionExtractor descriptionExtractor;
private final RepositoryMetadata repositoryMetadata;
private final DataFunctionRegistry functionRegistry;
private final RepositoryFactoryConfiguration configuration;
- public QueryMethodDataOperationResolver(QueryDescriptionExtractor descriptionExtractor, RepositoryMetadata repositoryMetadata, DataFunctionRegistry functionRegistry, RepositoryFactoryConfiguration configuration) {
+ public QueryMethodDataOperationResolver(MethodQueryDescriptionExtractor descriptionExtractor, RepositoryMetadata repositoryMetadata, DataFunctionRegistry functionRegistry, RepositoryFactoryConfiguration configuration) {
this.descriptionExtractor = descriptionExtractor;
this.repositoryMetadata = repositoryMetadata;
this.functionRegistry = functionRegistry;
@@ -50,7 +50,7 @@ public QueryMethodDataOperationResolver(QueryDescriptionExtractor descriptionExt
return null;
}
log.info("Extracting query description from the method by parsing the method");
- final QueryDescriptor descriptor = descriptionExtractor.extract(repositoryMetadata, method, configuration);
+ final QueryDescriptor descriptor = descriptionExtractor.extract(repositoryMetadata, configuration, method);
return new DescribedDataStoreOperation<>(new SelectDataStoreOperation<>(descriptor), functionRegistry);
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultPagingAndSortingRepository.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultPagingAndSortingRepository.java
index 76b13ef3..e74a5af4 100644
--- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultPagingAndSortingRepository.java
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultPagingAndSortingRepository.java
@@ -1,21 +1,14 @@
package com.mmnaseri.utils.spring.data.repository;
import com.mmnaseri.utils.spring.data.domain.DataStoreAware;
-import com.mmnaseri.utils.spring.data.domain.impl.PropertyComparator;
-import com.mmnaseri.utils.spring.data.query.NullHandling;
-import com.mmnaseri.utils.spring.data.query.Order;
-import com.mmnaseri.utils.spring.data.query.SortDirection;
-import com.mmnaseri.utils.spring.data.query.impl.ImmutableOrder;
-import com.mmnaseri.utils.spring.data.query.impl.ImmutableSort;
import com.mmnaseri.utils.spring.data.store.DataStore;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.domain.Page;
-import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
-import java.util.LinkedList;
+import java.util.Collection;
import java.util.List;
/**
@@ -23,7 +16,7 @@
* @since 1.0 (10/12/15)
*/
@SuppressWarnings("WeakerAccess")
-public class DefaultPagingAndSortingRepository implements DataStoreAware {
+public class DefaultPagingAndSortingRepository extends PagingAndSortingSupport implements DataStoreAware {
private static final Log log = LogFactory.getLog(DefaultPagingAndSortingRepository.class);
@@ -31,68 +24,32 @@ public class DefaultPagingAndSortingRepository implements DataStoreAware {
/**
* Finds everything and sorts it using the given sort property
- * @param sort how to sort the data
+ *
+ * @param sort how to sort the data
* @return sorted entries, unless sort is null.
*/
public List findAll(Sort sort) {
- log.info("Loading all the data in the data store");
- //noinspection unchecked
- final List list = new LinkedList(dataStore.retrieveAll());
- if (sort == null) {
- log.info("No sort was specified, so we are just going to return the data as-is");
- return list;
- }
- final List orders = new LinkedList<>();
- for (Sort.Order order : sort) {
- final SortDirection direction = order.getDirection().equals(Sort.Direction.ASC) ? SortDirection.ASCENDING : SortDirection.DESCENDING;
- final NullHandling nullHandling;
- switch (order.getNullHandling()) {
- case NULLS_FIRST:
- nullHandling = NullHandling.NULLS_FIRST;
- break;
- case NULLS_LAST:
- nullHandling = NullHandling.NULLS_LAST;
- break;
- default:
- nullHandling = NullHandling.DEFAULT;
- break;
- }
- final Order derivedOrder = new ImmutableOrder(direction, order.getProperty(), nullHandling);
- orders.add(derivedOrder);
- }
- log.info("Sorting the retrieved data: " + orders);
- PropertyComparator.sort(list, new ImmutableSort(orders));
- return list;
+ return PagingAndSortingUtils.sort(retrieveAll(), sort);
}
/**
* Loads everything, sorts them, and pages the according to the spec.
- * @param pageable the pagination and sort spec
+ *
+ * @param pageable the pagination and sort spec
* @return the specified view of the data
*/
public Page findAll(Pageable pageable) {
- final List> all;
- if (pageable.getSort() != null) {
- log.info("The page specification requests sorting, so we are going to sort the data first");
- all = findAll(pageable.getSort());
- } else {
- log.info("The page specification does not need sorting, so we are going to load the data as-is");
- //noinspection unchecked
- all = new LinkedList(dataStore.retrieveAll());
- }
- int start = Math.max(0, pageable.getPageNumber() * pageable.getPageSize());
- int end = start + pageable.getPageSize();
- start = Math.min(start, all.size());
- end = Math.min(end, all.size());
- log.info("Trimming the selection down for page " + pageable.getPageNumber() + " to include items from " + start + " to " + end);
- final List> selection = new LinkedList<>(all.subList(start, end));
- //noinspection unchecked
- return new PageImpl(selection, pageable, all.size());
+ return page(retrieveAll(), pageable);
}
@Override
public void setDataStore(DataStore dataStore) {
this.dataStore = dataStore;
}
-
+
+ private Collection retrieveAll() {
+ log.info("Loading all the data in the data store");
+ return dataStore.retrieveAll();
+ }
+
}
diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutor.java
new file mode 100644
index 00000000..dfacae05
--- /dev/null
+++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutor.java
@@ -0,0 +1,122 @@
+package com.mmnaseri.utils.spring.data.repository;
+
+import com.mmnaseri.utils.spring.data.domain.*;
+import com.mmnaseri.utils.spring.data.domain.impl.ImmutableInvocation;
+import com.mmnaseri.utils.spring.data.domain.impl.SelectDataStoreOperation;
+import com.mmnaseri.utils.spring.data.error.InvalidArgumentException;
+import com.mmnaseri.utils.spring.data.proxy.RepositoryConfiguration;
+import com.mmnaseri.utils.spring.data.proxy.RepositoryConfigurationAware;
+import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration;
+import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfigurationAware;
+import com.mmnaseri.utils.spring.data.query.QueryDescriptor;
+import com.mmnaseri.utils.spring.data.store.DataStore;
+import com.mmnaseri.utils.spring.data.tools.PropertyUtils;
+import org.springframework.data.domain.*;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Milad Naseri (milad.naseri@cdk.com)
+ * @since 1.0 (6/8/16, 11:57 AM)
+ */
+public class DefaultQueryByExampleExecutor implements DataStoreAware, RepositoryConfigurationAware, RepositoryMetadataAware, RepositoryFactoryConfigurationAware {
+
+ private DataStore dataStore;
+ private final ExampleMatcherQueryDescriptionExtractor queryDescriptionExtractor;
+ private RepositoryConfiguration repositoryConfiguration;
+ private RepositoryMetadata repositoryMetadata;
+ private RepositoryFactoryConfiguration configuration;
+
+ public DefaultQueryByExampleExecutor() {
+ queryDescriptionExtractor = new ExampleMatcherQueryDescriptionExtractor();
+ }
+
+ public Object findOne(Example example) {
+ final Collection found = retrieveAll(example);
+ if (found.isEmpty()) {
+ return null;
+ } else if (found.size() > 1) {
+ throw new InvalidArgumentException("Expected to see exactly one item found, but found " + found.size() + ". You should use a better example.");
+ }
+ return found.iterator().next();
+ }
+
+ public Iterable findAll(Example example) {
+ return retrieveAll(example);
+ }
+
+ public Iterable findAll(Example example, Sort sort) {
+ return PagingAndSortingUtils.sort(retrieveAll(example), sort);
+ }
+
+ public Page findAll(Example example, Pageable pageable) {
+ return PagingAndSortingUtils.page(retrieveAll(example), pageable);
+ }
+
+ public long count(Example example) {
+ return (long) retrieveAll(example).size();
+ }
+
+ public boolean exists(Example example) {
+ return count(example) > 0;
+ }
+
+ @Override
+ public void setDataStore(DataStore dataStore) {
+ //noinspection unchecked
+ this.dataStore = dataStore;
+ }
+
+ @Override
+ public void setRepositoryConfiguration(RepositoryConfiguration repositoryConfiguration) {
+ this.repositoryConfiguration = repositoryConfiguration;
+ }
+
+ @Override
+ public void setRepositoryMetadata(RepositoryMetadata repositoryMetadata) {
+ this.repositoryMetadata = repositoryMetadata;
+ }
+
+ @Override
+ public void setRepositoryFactoryConfiguration(RepositoryFactoryConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Retrieves all entities that match the given example
+ * @param example the example for finding the entities
+ * @return a collection of matched entities
+ */
+ private Collection> retrieveAll(Example example) {
+ final QueryDescriptor descriptor = queryDescriptionExtractor.extract(repositoryMetadata, configuration, example);
+ final Invocation invocation = createInvocation(descriptor, example);
+ final SelectDataStoreOperation select = new SelectDataStoreOperation<>(descriptor);
+ return select.execute(dataStore, repositoryConfiguration, invocation);
+ }
+
+ /**
+ * This method will create an invocation that had it occurred on a query method would provide sufficient
+ * data for a parsed query method expression to be evaluated
+ * @param descriptor the query descriptor
+ * @param example the example that is used for evaluation
+ * @return the fake method invocation corresponding to the example probe
+ */
+ private Invocation createInvocation(QueryDescriptor descriptor, Example example) {
+ final List