From a9f813af418556cf21c8e82e403214ba976c1f48 Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Mon, 18 Apr 2016 02:40:49 -0700 Subject: [PATCH 01/10] chore(travis): set `sudo` to `false` to enable container infrastructure --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a69d9b15..e5f5b785 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ after_success: - '[[ $TRAVIS_BRANCH == "master" && ( "x${TRAVIS_PULL_REQUEST}" == "xfalse" || "x${TRAVIS_PULL_REQUEST}" == "x" ) && ( "x$(echo $JAVA_HOME | grep -o 8)" == "x8" ) ]] && sudo apt-get install gnupg2' - '[[ $TRAVIS_BRANCH == "master" && ( "x${TRAVIS_PULL_REQUEST}" == "xfalse" || "x${TRAVIS_PULL_REQUEST}" == "x" ) && ( "x$(echo $JAVA_HOME | grep -o 8)" == "x8" ) ]] && bash ../deployment/deploy.sh eb1a6f34f056 ../deployment/key.asc.enc ../deployment/settings.xml' # SUDO should be set to `false` except when deploying to OSSRH to trigger container infrastructure -sudo: true +sudo: false env: global: - secure: DPkao3yJ4hhEsUOfs2VQJ8WCV3VdR3ToFiGB1l461PUstjaytTeXYK3bW2PmZqYJs6Hxxwyl8UHFvgBvWLb/yQV/dqyyJAV8XRb9UC37e4ddLoi8HE7NSGQIiXq0tySiMaOTyd2NtRTepfNAMEMDP746Nrox1giPEq1FPv+K98o= From 66f30298bac455d589ac76160462b9a6ddd7c952 Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Thu, 28 Apr 2016 21:43:53 -0700 Subject: [PATCH 02/10] feat(all): add support for QueryDslPredicateExecutor This goes toward #3 --- spring-data-mock/pom.xml | 30 +++ .../proxy/impl/DefaultTypeMappingContext.java | 9 +- .../DefaultPagingAndSortingRepository.java | 65 +----- .../DefaultQueryDslPredicateExecutor.java | 63 ++++++ .../repository/PagingAndSortingSupport.java | 90 ++++++++ .../impl/DefaultTypeMappingContextTest.java | 7 +- .../DefaultQueryDslPredicateExecutorTest.java | 205 ++++++++++++++++++ 7 files changed, 405 insertions(+), 64 deletions(-) create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutorTest.java diff --git a/spring-data-mock/pom.xml b/spring-data-mock/pom.xml index b96dd0a5..76d1f01e 100644 --- a/spring-data-mock/pom.xml +++ b/spring-data-mock/pom.xml @@ -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-logging commons-logging @@ -280,6 +308,8 @@ 2.8.2 1.6.7 2.7 + 4.1.0 + 3.2.2 \ No newline at end of file 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..e39c52e4 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; @@ -56,6 +53,10 @@ public DefaultTypeMappingContext(boolean registerDefaults) { 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", ClassUtils.getDefaultClassLoader())) { + log.debug("QueryDSL support is enabled. We will add the proper method implementations."); + register(Object.class, DefaultQueryDslPredicateExecutor.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/repository/DefaultPagingAndSortingRepository.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultPagingAndSortingRepository.java index 76b13ef3..03065059 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); @@ -35,34 +28,7 @@ public class DefaultPagingAndSortingRepository implements DataStoreAware { * @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 sort(retrieveAll(), sort); } /** @@ -71,28 +37,17 @@ public List findAll(Sort sort) { * @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/DefaultQueryDslPredicateExecutor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java new file mode 100644 index 00000000..06c065d0 --- /dev/null +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java @@ -0,0 +1,63 @@ +package com.mmnaseri.utils.spring.data.repository; + +import com.mmnaseri.utils.spring.data.domain.DataStoreAware; +import com.mmnaseri.utils.spring.data.store.DataStore; +import com.querydsl.collections.CollQuery; +import com.querydsl.collections.CollQueryFactory; +import com.querydsl.core.alias.Alias; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (4/28/16) + */ +public class DefaultQueryDslPredicateExecutor extends PagingAndSortingSupport implements DataStoreAware { + + private DataStore dataStore; + private Object alias; + + public Object findOne(Predicate predicate) { + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetchOne(); + } + + public Iterable findAll(Predicate predicate) { + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetch(); + } + + public Iterable findAll(Predicate predicate, Sort sort) { + return sort(((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetch(), sort); + } + + public Page findAll(Predicate predicate, Pageable pageable) { + return page(((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetch(), pageable); + } + + public long count(Predicate predicate) { + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetchCount(); + } + + public boolean exists(Predicate predicate) { + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetchCount() > 0; + } + + public Iterable findAll(OrderSpecifier... orders) { + //noinspection unchecked + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).orderBy(orders)).fetch(); + } + + public Iterable findAll(Predicate predicate, OrderSpecifier... orders) { + //noinspection unchecked + return ((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate).orderBy(orders)).fetch(); + } + + @Override + public void setDataStore(DataStore dataStore) { + this.dataStore = dataStore; + this.alias = Alias.alias(dataStore.getEntityType()); + } + +} diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java new file mode 100644 index 00000000..843c335f --- /dev/null +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java @@ -0,0 +1,90 @@ +package com.mmnaseri.utils.spring.data.repository; + +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 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.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (4/28/16) + */ +public class PagingAndSortingSupport { + + private static final Log log = LogFactory.getLog(PagingAndSortingSupport.class); + + /** + * Finds everything and sorts it using the given sort property + * @param entries the entries to be sorted + * @param sort how to sort the data + * @return sorted entries, unless sort is null. + */ + protected List sort(Collection entries, Sort sort) { + //noinspection unchecked + final List list = new LinkedList(entries); + 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; + } + + /** + * Loads everything, sorts them, and pages the according to the spec. + * @param entries the entries to be paged + * @param pageable the pagination and sort spec + * @return the specified view of the data + */ + public Page page(Collection entries, 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 = sort(entries, 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(entries); + } + 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()); + } + +} diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java index f66ce9c8..c740b1e9 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java @@ -2,10 +2,7 @@ import com.mmnaseri.utils.spring.data.error.RepositoryDefinitionException; import com.mmnaseri.utils.spring.data.proxy.TypeMapping; -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 com.mmnaseri.utils.spring.data.sample.usecases.proxy.*; import org.hamcrest.Matchers; import org.testng.annotations.BeforeMethod; @@ -26,7 +23,7 @@ public class DefaultTypeMappingContextTest { @BeforeMethod public void setUp() throws Exception { - defaultImplementations = new Class[]{DefaultGemfireRepository.class, DefaultJpaRepository.class, DefaultPagingAndSortingRepository.class, DefaultCrudRepository.class}; + defaultImplementations = new Class[]{DefaultGemfireRepository.class, DefaultJpaRepository.class, DefaultPagingAndSortingRepository.class, DefaultCrudRepository.class, DefaultQueryDslPredicateExecutor.class}; } @Test diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutorTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutorTest.java new file mode 100644 index 00000000..3ffa9659 --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutorTest.java @@ -0,0 +1,205 @@ +package com.mmnaseri.utils.spring.data.repository; + +import com.mmnaseri.utils.spring.data.sample.models.Person; +import com.mmnaseri.utils.spring.data.store.impl.MemoryDataStore; +import com.mmnaseri.utils.spring.data.utils.TestUtils; +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.types.dsl.BooleanExpression; +import org.hamcrest.Matchers; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.querydsl.core.alias.Alias.$; +import static com.querydsl.core.alias.Alias.alias; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (4/28/16) + */ +public class DefaultQueryDslPredicateExecutorTest { + + private MemoryDataStore dataStore; + private DefaultQueryDslPredicateExecutor executor; + + @BeforeMethod + public void setUp() throws Exception { + dataStore = new MemoryDataStore<>(Person.class); + executor = new DefaultQueryDslPredicateExecutor(); + executor.setDataStore(dataStore); + } + + @Test + public void testFindOne() throws Exception { + final Person saved = new Person().setId("1").setAge(10); + dataStore.save("1", saved); + dataStore.save("2", new Person().setId("2")); + final Person person = alias(Person.class); + final BooleanExpression predicate = $(person.getId()).eq("1"); + final Object one = executor.findOne(predicate); + assertThat(one, is(notNullValue())); + assertThat(one, Matchers.is(saved)); + } + + @Test + public void testFindOneWhenNoneExists() throws Exception { + final Person person = alias(Person.class); + final BooleanExpression predicate = $(person.getId()).eq("1"); + final Object one = executor.findOne(predicate); + assertThat(one, is(nullValue())); + } + + @Test(expectedExceptions = NonUniqueResultException.class) + public void testFindOneWhenMoreThanOneMatches() throws Exception { + dataStore.save("1", new Person().setId("1").setFirstName("Milad").setLastName("Naseri")); + dataStore.save("2", new Person().setId("2").setFirstName("Hassan").setLastName("Naseri")); + final Person person = alias(Person.class); + final BooleanExpression predicate = $(person.getLastName()).eq("Naseri"); + final Object one = executor.findOne(predicate); + assertThat(one, is(nullValue())); + } + + @Test + public void testFindAllUsingAPredicate() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(10)); + dataStore.save("2", new Person().setId("2").setAge(15)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(25)); + dataStore.save("5", new Person().setId("5").setAge(30)); + final Person person = alias(Person.class); + final Iterable result = executor.findAll($(person.getAge()).gt(20).or($(person.getAge()).eq(20))); + assertThat(result, is(notNullValue())); + final List list = TestUtils.iterableToList(result); + assertThat(list, hasSize(3)); + final List ids = new ArrayList<>(Arrays.asList("3", "4", "5")); + for (Object obj : list) { + assertThat(obj, is(instanceOf(Person.class))); + final Person loaded = (Person) obj; + assertThat(loaded.getId(), isIn(ids)); + ids.remove(loaded.getId()); + } + assertThat(ids, is(Matchers.empty())); + } + + @Test + public void testFindAllWithSort() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final Iterable result = executor.findAll($(person.getAge()).gt(20).or($(person.getAge()).eq(20)), new Sort(Sort.Direction.ASC, "age")); + assertThat(result, is(notNullValue())); + final List list = TestUtils.iterableToList(result); + assertThat(list, hasSize(3)); + assertThat(list.get(0), Matchers.is(dataStore.retrieve("3"))); + assertThat(list.get(1), Matchers.is(dataStore.retrieve("2"))); + assertThat(list.get(2), Matchers.is(dataStore.retrieve("1"))); + } + + @Test + public void testFindAllWithPagingAndSorting() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final Iterable result = executor.findAll($(person.getAge()).gt(20).or($(person.getAge()).eq(20)), new PageRequest(0, 2, new Sort(Sort.Direction.ASC, "age"))); + assertThat(result, is(notNullValue())); + final List list = TestUtils.iterableToList(result); + assertThat(list, hasSize(2)); + assertThat(list.get(0), Matchers.is(dataStore.retrieve("3"))); + assertThat(list.get(1), Matchers.is(dataStore.retrieve("2"))); + } + + @Test + public void testFindAllWithPaging() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final Iterable result = executor.findAll($(person.getAge()).gt(20).or($(person.getAge()).eq(20)), new PageRequest(0, 2)); + assertThat(result, is(notNullValue())); + final List list = TestUtils.iterableToList(result); + assertThat(list, hasSize(2)); + for (Object obj : list) { + assertThat(obj, is(instanceOf(Person.class))); + final Person loaded = (Person) obj; + assertThat(loaded, is(dataStore.retrieve(loaded.getId()))); + } + } + + @Test + public void testCount() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final long count = executor.count($(person.getAge()).gt(20).or($(person.getAge()).eq(20))); + assertThat(count, is((long) 3)); + } + + @Test + public void testExists() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + assertThat(executor.exists($(person.getAge()).gt(20).or($(person.getAge()).eq(20))), is(true)); + assertThat(executor.exists($(person.getAge()).lt(20).or($(person.getAge()).eq(20))), is(true)); + assertThat(executor.exists($(person.getAge()).lt(10).or($(person.getAge()).eq(10))), is(true)); + assertThat(executor.exists($(person.getAge()).lt(5).or($(person.getAge()).eq(5))), is(false)); + } + + @Test + public void testFindAllWithExplicitOrdering() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final Iterable all = executor.findAll($(person.getAge()).asc()); + assertThat(all, is(notNullValue())); + final List list = TestUtils.iterableToList(all); + assertThat(list, hasSize(5)); + assertThat(list.get(0), Matchers.is(dataStore.retrieve("5"))); + assertThat(list.get(1), Matchers.is(dataStore.retrieve("4"))); + assertThat(list.get(2), Matchers.is(dataStore.retrieve("3"))); + assertThat(list.get(3), Matchers.is(dataStore.retrieve("2"))); + assertThat(list.get(4), Matchers.is(dataStore.retrieve("1"))); + } + + @Test + public void testFindAllWithPredicateAndExplicitOrdering() throws Exception { + dataStore.save("1", new Person().setId("1").setAge(30)); + dataStore.save("2", new Person().setId("2").setAge(25)); + dataStore.save("3", new Person().setId("3").setAge(20)); + dataStore.save("4", new Person().setId("4").setAge(15)); + dataStore.save("5", new Person().setId("5").setAge(10)); + final Person person = alias(Person.class); + final Iterable all = executor.findAll($(person.getAge()).gt(20), $(person.getAge()).asc()); + assertThat(all, is(notNullValue())); + final List list = TestUtils.iterableToList(all); + assertThat(list, hasSize(2)); + assertThat(list.get(0), Matchers.is(dataStore.retrieve("2"))); + assertThat(list.get(1), Matchers.is(dataStore.retrieve("1"))); + } + +} \ No newline at end of file From 61fe5abe0d94c68bcf222b8e5335b90490327ba3 Mon Sep 17 00:00:00 2001 From: Paul Verest Date: Wed, 8 Jun 2016 14:24:47 +0800 Subject: [PATCH 03/10] :leapstick: README test --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cf82a000..2da8d375 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ or you can add a maven dependency since it is now available in Maven central: com.mmnaseri.utils spring-data-mock ${latest-version} + test ## Quick Start From a3ebbe7beec6a94d87e31332ef44274eb914ecaa Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Wed, 8 Jun 2016 01:03:16 -0700 Subject: [PATCH 04/10] chore(maven): update maven dependencies --- spring-data-mock/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-mock/pom.xml b/spring-data-mock/pom.xml index b96dd0a5..9131ebfb 100644 --- a/spring-data-mock/pom.xml +++ b/spring-data-mock/pom.xml @@ -270,10 +270,10 @@ 1.12.1.RELEASE 1.8.1.RELEASE 1.10.1.RELEASE - 2.7.3 + 2.7.4 6.9.10 1.3 - 2.9.3 + 2.9.4 1.6 2.10.3 3.0.0 From 3dd27064a11884f2bcb4313435a838ea2712ac03 Mon Sep 17 00:00:00 2001 From: Paul Verest Date: Wed, 8 Jun 2016 16:49:44 +0800 Subject: [PATCH 05/10] README - usage example For most users making it run is the 1st barrier --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 2da8d375..fceae4e8 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,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: From c2c999ef26a91ded0924b86ad8f52f12f7b19c1b Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Wed, 8 Jun 2016 02:10:20 -0700 Subject: [PATCH 06/10] fix(com.mmnaseri.utils.spring.data.domain.impl.id): add support for ID annotations from JPA fixes #55 --- spring-data-mock/pom.xml | 8 ++ .../id/AnnotatedFieldIdPropertyResolver.java | 6 +- .../id/AnnotatedGetterIdPropertyResolver.java | 11 ++- .../impl/id/AnnotatedIdPropertyResolver.java | 37 --------- .../impl/id/IdPropertyResolverUtils.java | 83 +++++++++++++++++++ .../id/NamedGetterIdPropertyResolver.java | 5 +- .../error/MultipleIdPropertiesException.java | 6 +- .../AnnotatedFieldIdPropertyResolverTest.java | 17 ++++ ...AnnotatedGetterIdPropertyResolverTest.java | 17 ++++ .../impl/id/IdPropertyResolverUtilsTest.java | 50 +++++++++++ .../EntityWithAnnotatedIdFieldFromJPA.java | 22 +++++ .../EntityWithAnnotatedIdGetterFromJPA.java | 22 +++++ 12 files changed, 236 insertions(+), 48 deletions(-) delete mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedIdPropertyResolver.java create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdFieldFromJPA.java create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdGetterFromJPA.java diff --git a/spring-data-mock/pom.xml b/spring-data-mock/pom.xml index ee13c5ce..eb57a560 100644 --- a/spring-data-mock/pom.xml +++ b/spring-data-mock/pom.xml @@ -153,6 +153,13 @@ joda-time joda-time ${joda-time.version} + provided + + + javax.persistence + persistence-api + ${persistence-api.version} + provided @@ -310,6 +317,7 @@ 2.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/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 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 idType) { @@ -26,11 +29,11 @@ public String resolve(final Class entityType, Class 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 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..bc9ba653 --- /dev/null +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java @@ -0,0 +1,83 @@ +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.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.Id; +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 String JAVAX_PERSISTENCE_ID = "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 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 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<>(); + annotations.add(Id.class); + final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + if (ClassUtils.isPresent(JAVAX_PERSISTENCE_ID, classLoader)) { + final Class loadedClass; + try { + loadedClass = ClassUtils.forName(JAVAX_PERSISTENCE_ID, classLoader); + annotations.add(loadedClass.asSubclass(Annotation.class)); + } catch (ClassNotFoundException ignored) { + } + } + 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 idType) { 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 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/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolverTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolverTest.java index 88acc648..b0d8f25f 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolverTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedFieldIdPropertyResolverTest.java @@ -2,8 +2,14 @@ import com.mmnaseri.utils.spring.data.domain.IdPropertyResolver; import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdField; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdFieldFromJPA; import com.mmnaseri.utils.spring.data.sample.models.EntityWithMultipleAnnotatedFields; import com.mmnaseri.utils.spring.data.sample.models.EntityWithoutAnnotatedFields; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; /** * @author Milad Naseri (mmnaseri@programmer.net) @@ -32,4 +38,15 @@ protected Class entityWithMultipleProperties() { return EntityWithMultipleAnnotatedFields.class; } + /** + * Regression test for https://github.com/mmnaseri/spring-data-mock/issues/55 + * @throws Exception + */ + @Test + public void testResolvingIdPropertyWhenIdAnnotationOnFieldIsFromJPA() throws Exception { + final String property = getIdPropertyResolver().resolve(EntityWithAnnotatedIdFieldFromJPA.class, Long.class); + assertThat(property, is(notNullValue())); + assertThat(property, is("customIdProperty")); + } + } \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolverTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolverTest.java index e69dc11a..13f8490c 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolverTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/AnnotatedGetterIdPropertyResolverTest.java @@ -2,8 +2,14 @@ import com.mmnaseri.utils.spring.data.domain.IdPropertyResolver; import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdGetter; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdGetterFromJPA; import com.mmnaseri.utils.spring.data.sample.models.EntityWithMultipleAnnotatedIdGetters; import com.mmnaseri.utils.spring.data.sample.models.EntityWithoutAnnotatedIdGetter; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; /** * @author Milad Naseri (mmnaseri@programmer.net) @@ -32,4 +38,15 @@ protected Class entityWithMultipleProperties() { return EntityWithMultipleAnnotatedIdGetters.class; } + /** + * Regression test for https://github.com/mmnaseri/spring-data-mock/issues/55 + * @throws Exception + */ + @Test + public void testResolvingIdPropertyWhenIdAnnotationOnGetterIsFromJPA() throws Exception { + final String property = getIdPropertyResolver().resolve(EntityWithAnnotatedIdGetterFromJPA.class, Integer.class); + assertThat(property, is(notNullValue())); + assertThat(property, is("myCustomId")); + } + } \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java new file mode 100644 index 00000000..df48ef2a --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java @@ -0,0 +1,50 @@ +package com.mmnaseri.utils.spring.data.domain.impl.id; + +import com.mmnaseri.utils.spring.data.error.PropertyTypeMismatchException; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdField; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdFieldFromJPA; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdGetter; +import com.mmnaseri.utils.spring.data.sample.models.EntityWithAnnotatedIdGetterFromJPA; +import com.mmnaseri.utils.spring.data.tools.AbstractUtilityClassTest; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (6/8/16, 1:49 AM) + */ +public class IdPropertyResolverUtilsTest extends AbstractUtilityClassTest { + + @Override + protected Class getUtilityClass() { + return IdPropertyResolverUtils.class; + } + + @Test + public void testReadingAnnotationFromField() throws Exception { + assertThat(IdPropertyResolverUtils.isAnnotated(EntityWithAnnotatedIdFieldFromJPA.class.getDeclaredField("customIdProperty")), is(true)); + assertThat(IdPropertyResolverUtils.isAnnotated(EntityWithAnnotatedIdField.class.getDeclaredField("id")), is(true)); + } + + @Test + public void testReadingAnnotationFromMethod() throws Exception { + assertThat(IdPropertyResolverUtils.isAnnotated(EntityWithAnnotatedIdGetterFromJPA.class.getDeclaredMethod("getMyCustomId")), is(true)); + assertThat(IdPropertyResolverUtils.isAnnotated(EntityWithAnnotatedIdGetter.class.getDeclaredMethod("getId")), is(true)); + } + + @Test(expectedExceptions = PropertyTypeMismatchException.class) + public void testPropertyNameFromMethodWhenIdTypeIsInvalid() throws Exception { + IdPropertyResolverUtils.getPropertyNameFromAnnotatedMethod(EntityWithAnnotatedIdGetterFromJPA.class, Long.class, EntityWithAnnotatedIdGetterFromJPA.class.getDeclaredMethod("getMyCustomId")); + } + + @Test + public void testPropertyNameFromMethod() throws Exception { + final String propertyName = IdPropertyResolverUtils.getPropertyNameFromAnnotatedMethod(EntityWithAnnotatedIdGetterFromJPA.class, Integer.class, EntityWithAnnotatedIdGetterFromJPA.class.getDeclaredMethod("getMyCustomId")); + assertThat(propertyName, is(notNullValue())); + assertThat(propertyName, is("myCustomId")); + } + +} \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdFieldFromJPA.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdFieldFromJPA.java new file mode 100644 index 00000000..d92305c3 --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdFieldFromJPA.java @@ -0,0 +1,22 @@ +package com.mmnaseri.utils.spring.data.sample.models; + +import javax.persistence.Id; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (6/8/16, 1:18 AM) + */ +public class EntityWithAnnotatedIdFieldFromJPA { + + @Id + private Long customIdProperty; + + public Long getCustomIdProperty() { + return customIdProperty; + } + + public void setCustomIdProperty(Long customIdProperty) { + this.customIdProperty = customIdProperty; + } + +} diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdGetterFromJPA.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdGetterFromJPA.java new file mode 100644 index 00000000..db2bf197 --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/models/EntityWithAnnotatedIdGetterFromJPA.java @@ -0,0 +1,22 @@ +package com.mmnaseri.utils.spring.data.sample.models; + +import javax.persistence.Id; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (6/8/16, 1:37 AM) + */ +public class EntityWithAnnotatedIdGetterFromJPA { + + private Integer myCustomId; + + @Id + public Integer getMyCustomId() { + return myCustomId; + } + + public void setMyCustomId(Integer myCustomId) { + this.myCustomId = myCustomId; + } + +} From 1088212887a188c136e0970a42fe3aae05e68fcc Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Wed, 8 Jun 2016 10:57:22 -0700 Subject: [PATCH 07/10] docs(all): update README with notes about dependencies This is to address #54 so that users know about how to interact with the framework when there is a conflict between their version of `commons-logging` and ours. --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fceae4e8..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) @@ -42,6 +42,27 @@ or you can add a maven dependency since it is now available in Maven central: 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: From 98d28e84c1a995e0f5e9371065995c1b96d47747 Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Thu, 9 Jun 2016 04:10:15 -0700 Subject: [PATCH 08/10] feat(all): add support for query by example This will address and close #56 properly --- .../impl/MethodQueryDescriptionExtractor.java | 399 ++++++++++++++++++ .../impl/QueryDescriptionExtractor.java | 395 +---------------- .../impl/id/IdPropertyResolverUtils.java | 22 +- .../data/dsl/factory/QueryDescription.java | 4 +- .../dsl/factory/RepositoryFactoryBuilder.java | 10 +- .../proxy/RepositoryFactoryConfiguration.java | 4 +- .../proxy/impl/DefaultRepositoryFactory.java | 4 +- ...DefaultRepositoryFactoryConfiguration.java | 8 +- .../proxy/impl/DefaultTypeMappingContext.java | 11 +- ...mutableRepositoryFactoryConfiguration.java | 8 +- .../DefaultDataOperationResolver.java | 4 +- .../QueryMethodDataOperationResolver.java | 8 +- .../DefaultPagingAndSortingRepository.java | 8 +- .../DefaultQueryByExampleExecutor.java | 122 ++++++ .../DefaultQueryDslPredicateExecutor.java | 2 +- ...ampleMatcherQueryDescriptionExtractor.java | 118 ++++++ .../repository/PagingAndSortingSupport.java | 82 +--- .../repository/PagingAndSortingUtils.java | 94 +++++ ... MethodQueryDescriptionExtractorTest.java} | 65 +-- .../impl/id/IdPropertyResolverUtilsTest.java | 14 + .../factory/RepositoryFactoryBuilderTest.java | 4 +- .../dsl/mock/RepositoryMockBuilderTest.java | 6 +- .../impl/DefaultRepositoryFactoryTest.java | 4 +- .../impl/DefaultTypeMappingContextTest.java | 9 +- ...bleRepositoryFactoryConfigurationTest.java | 4 +- .../DefaultDataOperationResolverTest.java | 2 +- .../QueryMethodDataOperationResolverTest.java | 6 +- .../DefaultQueryByExampleExecutorTest.java | 236 +++++++++++ .../repository/PagingAndSortingUtilsTest.java | 16 + ... NoOpMethodQueryDescriptionExtractor.java} | 8 +- 30 files changed, 1122 insertions(+), 555 deletions(-) create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractor.java create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutor.java create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/ExampleMatcherQueryDescriptionExtractor.java create mode 100644 spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtils.java rename spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/{QueryDescriptionExtractorTest.java => MethodQueryDescriptionExtractorTest.java} (81%) create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutorTest.java create mode 100644 spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtilsTest.java rename spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/{NoOpQueryDescriptionExtractor.java => NoOpMethodQueryDescriptionExtractor.java} (69%) 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:

+ * + *
    + *
  1. 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}
  2. + *
  3. 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.
    • + *
    + *
  4. + *
  5. 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.
  6. + *
  7. If the word {@literal Top} had appeared, we will look for the limit number, which should be an integer value.
  8. + *
  9. 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}
  10. + *
  11. 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.
  12. + *
  13. 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}. + *
  14. + *
  15. 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.
  16. + *
  17. 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.
  18. + *
  19. 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.
    • + *
    + *
  20. + *
+ * + * @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:

- * - *
    - *
  1. 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}
  2. - *
  3. 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.
    • - *
    - *
  4. - *
  5. 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.
  6. - *
  7. If the word {@literal Top} had appeared, we will look for the limit number, which should be an integer value.
  8. - *
  9. 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}
  10. - *
  11. 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.
  12. - *
  13. 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}. - *
  14. - *
  15. 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.
  16. - *
  17. 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.
  18. - *
  19. 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.
    • - *
    - *
  20. - *
- * - * @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/IdPropertyResolverUtils.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtils.java index bc9ba653..020e9e82 100644 --- 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 @@ -2,8 +2,9 @@ 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.data.annotation.Id; import org.springframework.util.ClassUtils; import java.io.Serializable; @@ -19,7 +20,13 @@ */ final class IdPropertyResolverUtils { - private static final String JAVAX_PERSISTENCE_ID = "javax.persistence.Id"; + 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(); @@ -67,14 +74,15 @@ static boolean isAnnotated(AnnotatedElement element) { */ private static List> getIdAnnotations() { final List> annotations = new ArrayList<>(); - annotations.add(Id.class); final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); - if (ClassUtils.isPresent(JAVAX_PERSISTENCE_ID, classLoader)) { - final Class loadedClass; + for (String idAnnotation : ID_ANNOTATIONS) { try { - loadedClass = ClassUtils.forName(JAVAX_PERSISTENCE_ID, classLoader); - annotations.add(loadedClass.asSubclass(Annotation.class)); + final Class type = ClassUtils.forName(idAnnotation, classLoader); + final Class 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/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/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 e39c52e4..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 @@ -45,18 +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", ClassUtils.getDefaultClassLoader())) { + 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 03065059..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 @@ -24,16 +24,18 @@ public class DefaultPagingAndSortingRepository extends PagingAndSortingSupport i /** * 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) { - return sort(retrieveAll(), sort); + 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) { 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 values = new ArrayList<>(); + //since according to http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example + //the matcher only supports AND condition, so, we expect to see only one branch + final List> branches = descriptor.getBranches(); + final List parameters = branches.get(0); + for (Parameter parameter : parameters) { + final String propertyPath = parameter.getPath(); + final Object propertyValue = PropertyUtils.getPropertyValue(example.getProbe(), propertyPath); + final ExampleMatcher.PropertySpecifier specifier = example.getMatcher().getPropertySpecifiers().getForPath(propertyPath); + values.add(specifier == null ? propertyValue : specifier.getPropertyValueTransformer().convert(propertyValue)); + } + return new ImmutableInvocation(null, values.toArray()); + } + +} diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java index 06c065d0..a2e861ac 100644 --- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryDslPredicateExecutor.java @@ -29,7 +29,7 @@ public Iterable findAll(Predicate predicate) { } public Iterable findAll(Predicate predicate, Sort sort) { - return sort(((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetch(), sort); + return PagingAndSortingUtils.sort(((CollQuery) CollQueryFactory.from(alias, dataStore.retrieveAll()).where(predicate)).fetch(), sort); } public Page findAll(Predicate predicate, Pageable pageable) { diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/ExampleMatcherQueryDescriptionExtractor.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/ExampleMatcherQueryDescriptionExtractor.java new file mode 100644 index 00000000..eec3dd6d --- /dev/null +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/ExampleMatcherQueryDescriptionExtractor.java @@ -0,0 +1,118 @@ +package com.mmnaseri.utils.spring.data.repository; + +import com.mmnaseri.utils.spring.data.domain.*; +import com.mmnaseri.utils.spring.data.domain.impl.ImmutableParameter; +import com.mmnaseri.utils.spring.data.domain.impl.QueryDescriptionExtractor; +import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration; +import com.mmnaseri.utils.spring.data.query.QueryDescriptor; +import com.mmnaseri.utils.spring.data.query.impl.DefaultQueryDescriptor; +import com.mmnaseri.utils.spring.data.tools.PropertyUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; + +import java.beans.PropertyDescriptor; +import java.util.*; + +/** + * @author Milad Naseri (milad.naseri@cdk.com) + * @since 1.0 (6/8/16, 12:45 PM) + */ +public class ExampleMatcherQueryDescriptionExtractor implements QueryDescriptionExtractor { + + @Override + public QueryDescriptor extract(RepositoryMetadata repositoryMetadata, RepositoryFactoryConfiguration configuration, Example example) { + final OperatorContext operatorContext = configuration.getDescriptionExtractor().getOperatorContext(); + final Map values = extractValues(example.getProbe()); + final ExampleMatcher matcher = example.getMatcher(); + final List parameters = new ArrayList<>(); + int index = 0; + for (Map.Entry entry : values.entrySet()) { + final String propertyPath = entry.getKey(); + if (matcher.isIgnoredPath(propertyPath)) { + continue; + } + final Set modifiers = new HashSet<>(); + final Operator operator; + if (entry.getValue() == null) { + if (ExampleMatcher.NullHandler.IGNORE.equals(matcher.getNullHandler())) { + continue; + } else { + operator = operatorContext.getBySuffix("IsNull"); + } + } else { + if (ignoreCase(matcher, propertyPath)) { + modifiers.add(Modifier.IGNORE_CASE); + } + final ExampleMatcher.StringMatcher stringMatcher = stringMatcher(matcher, propertyPath); + if (ExampleMatcher.StringMatcher.STARTING.equals(stringMatcher)) { + operator = operatorContext.getBySuffix("StartsWith"); + } else if (ExampleMatcher.StringMatcher.ENDING.equals(stringMatcher)) { + operator = operatorContext.getBySuffix("EndsWith"); + } else if (ExampleMatcher.StringMatcher.CONTAINING.equals(stringMatcher)) { + operator = operatorContext.getBySuffix("Contains"); + } else if (ExampleMatcher.StringMatcher.REGEX.equals(stringMatcher)) { + operator = operatorContext.getBySuffix("Matches"); + } else { + operator = operatorContext.getBySuffix("Is"); + } + } + parameters.add(new ImmutableParameter(propertyPath, modifiers, new int[]{index ++}, operator)); + } + return new DefaultQueryDescriptor(false, null, 0, null, null, Collections.singletonList(parameters), configuration, repositoryMetadata); + } + + private ExampleMatcher.StringMatcher stringMatcher(ExampleMatcher matcher, String path) { + final ExampleMatcher.PropertySpecifier specifier = matcher.getPropertySpecifiers().getForPath(path); + return specifier != null ? specifier.getStringMatcher() : matcher.getDefaultStringMatcher(); + } + + private boolean ignoreCase(ExampleMatcher matcher, String path) { + final ExampleMatcher.PropertySpecifier specifier = matcher.getPropertySpecifiers().getForPath(path); + return matcher.isIgnoreCaseEnabled() || specifier != null && Boolean.TRUE.equals(specifier.getIgnoreCase()); + } + + /** + * Given an input object, this method will return a map from the property paths to their corresponding values + * @param object the input object + * @return the map of values + */ + private Map extractValues(Object object) { + final Map result = new HashMap<>(); + final BeanWrapper wrapper = new BeanWrapperImpl(object); + for (PropertyDescriptor descriptor : wrapper.getPropertyDescriptors()) { + if (descriptor.getReadMethod() == null || descriptor.getWriteMethod() == null) { + continue; + } + final String propertyName = descriptor.getName(); + final Object value = PropertyUtils.getPropertyValue(object, propertyName); + if (value == null) { + result.put(propertyName, null); + continue; + } + if (isIntractable(descriptor, value)) { + result.put(propertyName, value); + continue; + } + final Map children = extractValues(value); + for (Map.Entry entry : children.entrySet()) { + result.put(propertyName + "." + entry.getKey(), entry.getValue()); + } + } + return result; + } + + /** + * This method is used to determine if a given value should be broken down further + * or should it be passed in as it is + * @param descriptor the descriptor for the property + * @param value the value for the property + * @return {@literal true} if the value should be left alone + */ + private boolean isIntractable(PropertyDescriptor descriptor, Object value) { + final Class type = descriptor.getPropertyType(); + return type.isPrimitive() || type.getName().startsWith("java.lang.") || value instanceof Iterable || value instanceof Map; + } + +} diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java index 843c335f..263f2387 100644 --- a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingSupport.java @@ -1,90 +1,18 @@ package com.mmnaseri.utils.spring.data.repository; -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 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.Collection; -import java.util.LinkedList; -import java.util.List; /** - * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) - * @since 1.0 (4/28/16) + * @author Milad Naseri (milad.naseri@cdk.com) + * @since 1.0 (6/8/16, 11:50 AM) */ -public class PagingAndSortingSupport { +public abstract class PagingAndSortingSupport { - private static final Log log = LogFactory.getLog(PagingAndSortingSupport.class); - - /** - * Finds everything and sorts it using the given sort property - * @param entries the entries to be sorted - * @param sort how to sort the data - * @return sorted entries, unless sort is null. - */ - protected List sort(Collection entries, Sort sort) { - //noinspection unchecked - final List list = new LinkedList(entries); - 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; - } - - /** - * Loads everything, sorts them, and pages the according to the spec. - * @param entries the entries to be paged - * @param pageable the pagination and sort spec - * @return the specified view of the data - */ - public Page page(Collection entries, 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 = sort(entries, 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(entries); - } - 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()); + public static Page page(Collection entries, Pageable pageable) { + return PagingAndSortingUtils.page(entries, pageable); } } diff --git a/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtils.java b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtils.java new file mode 100644 index 00000000..21d72374 --- /dev/null +++ b/spring-data-mock/src/main/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtils.java @@ -0,0 +1,94 @@ +package com.mmnaseri.utils.spring.data.repository; + +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 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.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Mohammad Milad Naseri (m.m.naseri@gmail.com) + * @since 1.0 (4/28/16) + */ +final class PagingAndSortingUtils { + + private static final Log log = LogFactory.getLog(PagingAndSortingUtils.class); + + private PagingAndSortingUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Finds everything and sorts it using the given sort property + * @param entries the entries to be sorted + * @param sort how to sort the data + * @return sorted entries, unless sort is null. + */ + public static List sort(Collection entries, Sort sort) { + //noinspection unchecked + final List list = new LinkedList(entries); + 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; + } + + /** + * Loads everything, sorts them, and pages the according to the spec. + * @param entries the entries to be paged + * @param pageable the pagination and sort spec + * @return the specified view of the data + */ + public static Page page(Collection entries, 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 = sort(entries, 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(entries); + } + 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()); + } + +} diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractorTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractorTest.java similarity index 81% rename from spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractorTest.java rename to spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractorTest.java index 50e83c6c..5384c7ec 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/QueryDescriptionExtractorTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/MethodQueryDescriptionExtractorTest.java @@ -15,6 +15,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.lang.reflect.Method; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; @@ -24,16 +25,16 @@ * @author Milad Naseri (mmnaseri@programmer.net) * @since 1.0 (9/21/15) */ -public class QueryDescriptionExtractorTest { +public class MethodQueryDescriptionExtractorTest { - private QueryDescriptionExtractor extractor; + private QueryDescriptionExtractor extractor; private RepositoryMetadata malformedRepositoryMetadata; private RepositoryMetadata sampleRepositoryMetadata; private RepositoryFactoryConfiguration configuration; @BeforeMethod public void setUp() throws Exception { - extractor = new QueryDescriptionExtractor(new DefaultOperatorContext()); + extractor = new MethodQueryDescriptionExtractor(new DefaultOperatorContext()); malformedRepositoryMetadata = new ImmutableRepositoryMetadata(String.class, Person.class, MalformedRepository.class, "id"); sampleRepositoryMetadata = new ImmutableRepositoryMetadata(String.class, Person.class, RepositoryWithValidMethods.class, "id"); } @@ -41,97 +42,97 @@ public void setUp() throws Exception { @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Malformed query method name.*") public void testMethodNameNotStartingWithNormalWord() throws Exception { configuration = new DefaultRepositoryFactoryConfiguration(); - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("Malformed"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("Malformed")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: There is already a limit of 5 specified for this query:.*") public void testMultipleLimitsUsingFirst() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findFirst5First10"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findFirst5First10")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: There is already a limit of 1 specified for this query:.*") public void testMultipleLimitsUsingFirstOne() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findFirstFirst10"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findFirstFirst10")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: There is already a limit of 10 specified for this query:.*") public void testMultipleLimits() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findTop10Top5"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findTop10Top5")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: You have already stated that this query should return distinct items:.*") public void testMultipleDistinctFlags() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findDistinctDistinct"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findDistinctDistinct")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Query method name cannot end with `By`") public void testNonSimpleQueryEndingInBy() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findTop10DistinctBy"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findTop10DistinctBy")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Could not find property `unknownProperty`.*") public void testUnknownPropertyInExpression() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByUnknownProperty"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByUnknownProperty")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Expected to see parameter with index 0") public void testTooFewParameterNumber() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstName"), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstName")); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Expected more tokens to follow AND/OR operator") public void testTooFewExpressionEndingInOr() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOr", String.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOr", String.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Expected more tokens to follow AND/OR operator") public void testTooFewExpressionEndingInAnd() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOr", String.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOr", String.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Expected parameter 0 on .*? to be a descendant of class .*?\\.String") public void testBadParameterType() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstName", Object.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstName", Object.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Invalid last argument: expected paging or sorting.*?") public void testBadLastParameter() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstName", String.class, Object.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstName", String.class, Object.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Too many parameters.*?") public void testTooManyParameters() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstName", String.class, Object.class, Object.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstName", String.class, Object.class, Object.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Could not find property `firstNameOrderBy` on `class com.mmnaseri.utils.spring.data.sample.models.Person`") public void testWithTrailingOrderBy() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOrderBy", String.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOrderBy", String.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Failed to get a property descriptor for expression: Xyz") public void testWithOrderByInvalidProperty() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOrderByXyzDesc", String.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOrderByXyzDesc", String.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: You cannot specify both an order-by clause and a dynamic ordering") public void testWithMultipleOrders() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOrderByFirstNameAsc", String.class, org.springframework.data.domain.Sort.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOrderByFirstNameAsc", String.class, org.springframework.data.domain.Sort.class)); } @Test(expectedExceptions = QueryParserException.class, expectedExceptionsMessageRegExp = ".*?: Sort property `address` is not comparable in `findByFirstNameOrderByAddressAsc`") public void testWithOrderByNonComparableProperty() throws Exception { - extractor.extract(malformedRepositoryMetadata, MalformedRepository.class.getMethod("findByFirstNameOrderByAddressAsc", String.class), configuration); + extractor.extract(malformedRepositoryMetadata, configuration, MalformedRepository.class.getMethod("findByFirstNameOrderByAddressAsc", String.class)); } @Test public void testIntegrity() throws Exception { - assertThat(extractor.getOperatorContext(), is(notNullValue())); + assertThat(((MethodQueryDescriptionExtractor) extractor).getOperatorContext(), is(notNullValue())); } @Test public void testReadMethodWithoutAnyCriteria() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("find"), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("find")); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getConfiguration(), is(configuration)); assertThat(descriptor.getFunction(), is(nullValue())); @@ -142,7 +143,7 @@ public void testReadMethodWithoutAnyCriteria() throws Exception { @Test public void testCustomFunctionWithoutAnyCriteria() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("test"), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("test")); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getConfiguration(), is(configuration)); assertThat(descriptor.getFunction(), is("test")); @@ -153,7 +154,7 @@ public void testCustomFunctionWithoutAnyCriteria() throws Exception { @Test public void testQueryMethodWithSingleBranch() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameEquals", String.class, String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameEquals", String.class, String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getConfiguration(), is(configuration)); assertThat(descriptor.getFunction(), is(nullValue())); @@ -176,7 +177,7 @@ public void testQueryMethodWithSingleBranch() throws Exception { @Test public void testModifierOnSingleParameter() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameIgnoreCase", String.class, String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameIgnoreCase", String.class, String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getConfiguration(), is(configuration)); assertThat(descriptor.getFunction(), is(nullValue())); @@ -200,7 +201,7 @@ public void testModifierOnSingleParameter() throws Exception { @Test public void testWithOrderBy() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrderByLastNameDescAgeAsc", String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrderByLastNameDescAgeAsc", String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(1)); @@ -221,7 +222,7 @@ public void testWithOrderBy() throws Exception { @Test public void testWithMultipleBranches() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrLastName", String.class, String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrLastName", String.class, String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(2)); assertThat(descriptor.getBranches().get(0), hasSize(1)); @@ -242,7 +243,7 @@ public void testWithMultipleBranches() throws Exception { @Test public void testWithStaticSortingAndDynamicPaging() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrderByLastNameDesc", String.class, Pageable.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameOrderByLastNameDesc", String.class, Pageable.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(1)); @@ -265,7 +266,7 @@ public void testWithStaticSortingAndDynamicPaging() throws Exception { @Test public void testWithDynamicSortingAndDynamicPaging() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstName", String.class, Pageable.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstName", String.class, Pageable.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(1)); @@ -291,7 +292,7 @@ public void testWithDynamicSortingAndDynamicPaging() throws Exception { @Test public void testWithDynamicSortAndNoPaging() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstName", String.class, org.springframework.data.domain.Sort.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstName", String.class, org.springframework.data.domain.Sort.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(1)); @@ -315,7 +316,7 @@ public void testWithDynamicSortAndNoPaging() throws Exception { @Test public void testAllIgnoreCase() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameAllIgnoreCase", String.class, String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("findByFirstNameAndLastNameAllIgnoreCase", String.class, String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(2)); @@ -335,7 +336,7 @@ public void testAllIgnoreCase() throws Exception { @Test public void testFunction() throws Exception { - final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, RepositoryWithValidMethods.class.getMethod("functionNameByFirstName", String.class), configuration); + final QueryDescriptor descriptor = extractor.extract(sampleRepositoryMetadata, configuration, RepositoryWithValidMethods.class.getMethod("functionNameByFirstName", String.class)); assertThat(descriptor, is(notNullValue())); assertThat(descriptor.getBranches(), hasSize(1)); assertThat(descriptor.getBranches().get(0), hasSize(1)); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java index df48ef2a..ffdc9daf 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/domain/impl/id/IdPropertyResolverUtilsTest.java @@ -8,6 +8,9 @@ import com.mmnaseri.utils.spring.data.tools.AbstractUtilityClassTest; import org.testng.annotations.Test; +import java.lang.reflect.Field; +import java.util.List; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -47,4 +50,15 @@ public void testPropertyNameFromMethod() throws Exception { assertThat(propertyName, is("myCustomId")); } + @Test + public void testDeclaringAnnotationsThatAreNotPresent() throws Exception { + final Field idAnnotations = IdPropertyResolverUtils.class.getDeclaredField("ID_ANNOTATIONS"); + idAnnotations.setAccessible(true); + final List list = (List) idAnnotations.get(null); + //noinspection unchecked + list.add("random class name"); + final String propertyName = IdPropertyResolverUtils.getPropertyNameFromAnnotatedMethod(EntityWithAnnotatedIdGetterFromJPA.class, Integer.class, EntityWithAnnotatedIdGetterFromJPA.class.getDeclaredMethod("getMyCustomId")); + assertThat(propertyName, is(notNullValue())); + } + } \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilderTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilderTest.java index d53b88b1..536eb31c 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilderTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/factory/RepositoryFactoryBuilderTest.java @@ -7,7 +7,7 @@ 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.ImmutableOperator; -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.RepositoryMockBuilder; import com.mmnaseri.utils.spring.data.proxy.RepositoryFactory; import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration; @@ -80,7 +80,7 @@ public void testUsingCustomMetadataResolver() throws Exception { @Test public void testUsingCustomQueryDescriptor() throws Exception { - final QueryDescriptionExtractor queryDescriptionExtractor = new QueryDescriptionExtractor(new DefaultOperatorContext()); + final MethodQueryDescriptionExtractor queryDescriptionExtractor = new MethodQueryDescriptionExtractor(new DefaultOperatorContext()); final RepositoryFactory factory = RepositoryFactoryBuilder.builder().extractQueriesUsing(queryDescriptionExtractor).build(); assertThat(factory, is(notNullValue())); assertThat(factory.getConfiguration(), is(notNullValue())); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/mock/RepositoryMockBuilderTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/mock/RepositoryMockBuilderTest.java index acd1f1c5..79b8a427 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/mock/RepositoryMockBuilderTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/dsl/mock/RepositoryMockBuilderTest.java @@ -4,7 +4,7 @@ import com.mmnaseri.utils.spring.data.domain.RepositoryAware; 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.error.CorruptDataException; import com.mmnaseri.utils.spring.data.error.DataOperationExecutionException; import com.mmnaseri.utils.spring.data.error.MockBuilderException; @@ -114,7 +114,7 @@ public void testUsingCustomImplementations() throws Exception { public void testUsingCustomFactory() throws Exception { final DefaultRepositoryFactoryConfiguration configuration = new DefaultRepositoryFactoryConfiguration(); configuration.setDataStoreRegistry(new DefaultDataStoreRegistry()); - configuration.setDescriptionExtractor(new QueryDescriptionExtractor(new DefaultOperatorContext())); + configuration.setDescriptionExtractor(new MethodQueryDescriptionExtractor(new DefaultOperatorContext())); configuration.setEventListenerContext(new DefaultDataStoreEventListenerContext()); configuration.setFunctionRegistry(new DefaultDataFunctionRegistry()); configuration.setOperationInvocationHandler(new NonDataOperationInvocationHandler()); @@ -143,7 +143,7 @@ public void testUsingCustomConfiguration() throws Exception { mappingContext.register(ConfiguredSimpleCrudPersonRepository.class, ConfigurationAwareMapper.class); final DefaultRepositoryFactoryConfiguration configuration = new DefaultRepositoryFactoryConfiguration(); configuration.setDataStoreRegistry(new DefaultDataStoreRegistry()); - configuration.setDescriptionExtractor(new QueryDescriptionExtractor(new DefaultOperatorContext())); + configuration.setDescriptionExtractor(new MethodQueryDescriptionExtractor(new DefaultOperatorContext())); configuration.setEventListenerContext(new DefaultDataStoreEventListenerContext()); configuration.setFunctionRegistry(new DefaultDataFunctionRegistry()); configuration.setOperationInvocationHandler(new NonDataOperationInvocationHandler()); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryTest.java index c1148a5e..70f40a31 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultRepositoryFactoryTest.java @@ -2,7 +2,7 @@ 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.domain.impl.key.UUIDKeyGenerator; import com.mmnaseri.utils.spring.data.proxy.RepositoryFactoryConfiguration; import com.mmnaseri.utils.spring.data.query.impl.DefaultDataFunctionRegistry; @@ -32,7 +32,7 @@ public void testRepositoryInstance() throws Exception { final DefaultRepositoryFactoryConfiguration configuration = new DefaultRepositoryFactoryConfiguration(); final DefaultDataStoreRegistry dataStoreRegistry = new DefaultDataStoreRegistry(); configuration.setDataStoreRegistry(dataStoreRegistry); - configuration.setDescriptionExtractor(new QueryDescriptionExtractor(new DefaultOperatorContext())); + configuration.setDescriptionExtractor(new MethodQueryDescriptionExtractor(new DefaultOperatorContext())); configuration.setEventListenerContext(new DefaultDataStoreEventListenerContext()); configuration.setFunctionRegistry(new DefaultDataFunctionRegistry()); configuration.setOperationInvocationHandler(new NonDataOperationInvocationHandler()); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java index c740b1e9..15367e72 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/DefaultTypeMappingContextTest.java @@ -23,7 +23,14 @@ public class DefaultTypeMappingContextTest { @BeforeMethod public void setUp() throws Exception { - defaultImplementations = new Class[]{DefaultGemfireRepository.class, DefaultJpaRepository.class, DefaultPagingAndSortingRepository.class, DefaultCrudRepository.class, DefaultQueryDslPredicateExecutor.class}; + defaultImplementations = new Class[]{ + DefaultGemfireRepository.class, + DefaultJpaRepository.class, + DefaultPagingAndSortingRepository.class, + DefaultCrudRepository.class, + DefaultQueryDslPredicateExecutor.class, + DefaultQueryByExampleExecutor.class + }; } @Test diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfigurationTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfigurationTest.java index 87cafca8..bc0b0618 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfigurationTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/ImmutableRepositoryFactoryConfigurationTest.java @@ -2,7 +2,7 @@ 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.query.impl.DefaultDataFunctionRegistry; import com.mmnaseri.utils.spring.data.store.impl.DefaultDataStoreEventListenerContext; import com.mmnaseri.utils.spring.data.store.impl.DefaultDataStoreRegistry; @@ -20,7 +20,7 @@ public class ImmutableRepositoryFactoryConfigurationTest { @Test public void testCopyConstructor() throws Exception { final DefaultRepositoryFactoryConfiguration original = new DefaultRepositoryFactoryConfiguration(); - original.setDescriptionExtractor(new QueryDescriptionExtractor(new DefaultOperatorContext())); + original.setDescriptionExtractor(new MethodQueryDescriptionExtractor(new DefaultOperatorContext())); original.setEventListenerContext(new DefaultDataStoreEventListenerContext()); original.setFunctionRegistry(new DefaultDataFunctionRegistry()); original.setRepositoryMetadataResolver(new DefaultRepositoryMetadataResolver()); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolverTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolverTest.java index 860eca79..cf944ff4 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolverTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/DefaultDataOperationResolverTest.java @@ -31,7 +31,7 @@ public class DefaultDataOperationResolverTest { public void setUp() throws Exception { final ArrayList> mappings = new ArrayList<>(); mappings.add(new ImmutableTypeMapping<>(MappedType.class, new MappedType())); - final QueryDescriptionExtractor descriptionExtractor = new QueryDescriptionExtractor(new DefaultOperatorContext()); + final MethodQueryDescriptionExtractor descriptionExtractor = new MethodQueryDescriptionExtractor(new DefaultOperatorContext()); final ImmutableRepositoryMetadata repositoryMetadata = new ImmutableRepositoryMetadata(String.class, Person.class, SampleMappedRepository.class, "id"); final DefaultDataFunctionRegistry functionRegistry = new DefaultDataFunctionRegistry(); resolver = new DefaultDataOperationResolver(mappings, descriptionExtractor, repositoryMetadata, functionRegistry, new DefaultRepositoryFactoryConfiguration()); diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolverTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolverTest.java index a38c244a..e828e73f 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolverTest.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/proxy/impl/resolvers/QueryMethodDataOperationResolverTest.java @@ -1,7 +1,7 @@ package com.mmnaseri.utils.spring.data.proxy.impl.resolvers; import com.mmnaseri.utils.spring.data.domain.impl.DescribedDataStoreOperation; -import com.mmnaseri.utils.spring.data.sample.usecases.proxy.resolvers.NoOpQueryDescriptionExtractor; +import com.mmnaseri.utils.spring.data.sample.usecases.proxy.resolvers.NoOpMethodQueryDescriptionExtractor; import com.mmnaseri.utils.spring.data.sample.usecases.proxy.resolvers.SampleMappedRepository; import com.mmnaseri.utils.spring.data.store.DataStoreOperation; import org.testng.annotations.Test; @@ -17,7 +17,7 @@ public class QueryMethodDataOperationResolverTest { @Test public void testThatItCallsAQueryExtractor() throws Exception { - final NoOpQueryDescriptionExtractor extractor = new NoOpQueryDescriptionExtractor(); + final NoOpMethodQueryDescriptionExtractor extractor = new NoOpMethodQueryDescriptionExtractor(); final QueryMethodDataOperationResolver resolver = new QueryMethodDataOperationResolver(extractor, null, null, null); assertThat(extractor.isCalled(), is(false)); final DataStoreOperation operation = resolver.resolve(SampleMappedRepository.class.getMethod("normalMethodBy")); @@ -28,7 +28,7 @@ public void testThatItCallsAQueryExtractor() throws Exception { @Test public void testMethodThatIsAnnotatedWithVendorQuery() throws Exception { - final QueryMethodDataOperationResolver resolver = new QueryMethodDataOperationResolver(new NoOpQueryDescriptionExtractor(), null, null, null); + final QueryMethodDataOperationResolver resolver = new QueryMethodDataOperationResolver(new NoOpMethodQueryDescriptionExtractor(), null, null, null); final DataStoreOperation method = resolver.resolve(SampleMappedRepository.class.getMethod("nativeMethod")); assertThat(method, is(nullValue())); } diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutorTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutorTest.java new file mode 100644 index 00000000..2679cfb7 --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/DefaultQueryByExampleExecutorTest.java @@ -0,0 +1,236 @@ +package com.mmnaseri.utils.spring.data.repository; + +import com.mmnaseri.utils.spring.data.domain.RepositoryMetadata; +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.MethodQueryDescriptionExtractor; +import com.mmnaseri.utils.spring.data.error.InvalidArgumentException; +import com.mmnaseri.utils.spring.data.proxy.impl.DefaultRepositoryFactoryConfiguration; +import com.mmnaseri.utils.spring.data.proxy.impl.ImmutableRepositoryConfiguration; +import com.mmnaseri.utils.spring.data.sample.models.Address; +import com.mmnaseri.utils.spring.data.sample.models.Person; +import com.mmnaseri.utils.spring.data.sample.models.State; +import com.mmnaseri.utils.spring.data.sample.models.Zip; +import com.mmnaseri.utils.spring.data.sample.repositories.SimplePersonRepository; +import com.mmnaseri.utils.spring.data.store.DataStore; +import com.mmnaseri.utils.spring.data.store.impl.MemoryDataStore; +import com.mmnaseri.utils.spring.data.utils.TestUtils; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.Serializable; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * @author Milad Naseri (milad.naseri@cdk.com) + * @since 1.0 (6/8/16, 12:12 PM) + */ +public class DefaultQueryByExampleExecutorTest { + + private DefaultQueryByExampleExecutor executor; + private DataStore dataStore; + + @BeforeMethod + public void setUp() throws Exception { + dataStore = new MemoryDataStore<>(Person.class); + dataStore.save("1", + new Person() + .setId("1") + .setFirstName("Milad") + .setLastName("Naseri") + .setAge(28) + .setAddressZip(new Zip()) + .setAddress( + new Address() + .setCity("Tehran") + .setState( + new State() + .setName("Teheran") + ) + ) + ); + dataStore.save("2", + new Person() + .setId("2") + .setFirstName("Zohreh") + .setLastName("Sadeghi") + .setAge(26) + .setAddressZip(new Zip()) + .setAddress( + new Address() + .setCity("Edmonds") + .setState( + new State() + .setName("WA") + ) + ) + ); + dataStore.save("3", + new Person() + .setId("3") + .setFirstName("Ramin") + .setLastName("Farhanian") + .setAge(40) + .setAddressZip(null) + .setAddress( + new Address() + .setCity("Kirkland") + .setState( + new State() + .setName("WA") + ) + ) + ); + dataStore.save("4", + new Person() + .setId("4") + .setFirstName("Niloufar") + .setLastName("Poursultani") + .setAge(53) + .setAddressZip(new Zip()) + .setAddress( + new Address() + .setCity("Tehran") + .setState( + new State() + .setName("Teheran") + ) + ) + ); + executor = new DefaultQueryByExampleExecutor(); + executor.setDataStore(dataStore); + final RepositoryMetadata metadata = new DefaultRepositoryMetadataResolver().resolve(SimplePersonRepository.class); + executor.setRepositoryMetadata(metadata); + executor.setRepositoryConfiguration(new ImmutableRepositoryConfiguration(metadata, null, null)); + final DefaultRepositoryFactoryConfiguration configuration = new DefaultRepositoryFactoryConfiguration(); + configuration.setDescriptionExtractor(new MethodQueryDescriptionExtractor(new DefaultOperatorContext())); + executor.setRepositoryFactoryConfiguration(configuration); + } + + @Test + public void testFindOneWhenThereIsNoMatch() throws Exception { + final Object result = executor.findOne(Example.of(new Person().setFirstName("Gigili").setLastName("Magooli"))); + assertThat(result, is(nullValue())); + } + + @Test(expectedExceptions = InvalidArgumentException.class) + public void testFindOneWhenThereIsMoreThanOneMatch() throws Exception { + executor.findOne(Example.of(new Person().setAddress(new Address().setCity("Tehran")))); + } + + @Test + public void testFindOne() throws Exception { + final Object found = executor.findOne(Example.of(new Person().setAddress(new Address().setCity("Tehran")).setFirstName("Milad"))); + assertThat(found, is(notNullValue())); + assertThat(found, is((Object) dataStore.retrieve("1"))); + } + + @Test + public void testFindByExampleUsingEndingWithMatcher() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("ran"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.endsWith()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(2)); + } + + @Test + public void testFindByExampleUsingStartingWithMatcher() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("Teh"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.startsWith()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(2)); + } + + @Test + public void testFindByExampleUsingContainingMatcher() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("ehe"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.contains()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(2)); + } + + @Test + public void testFindByExampleUsingRegexMatcher() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("(WA|Teheran)"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.regex()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(4)); + } + + @Test + public void testFindByExampleWithSorting() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("(WA|Teheran)"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.regex()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example, new Sort(Sort.Direction.ASC, "firstName", "lastName", "address.state.name")); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(4)); + assertThat(list.get(0), is((Object) dataStore.retrieve("1"))); + assertThat(list.get(1), is((Object) dataStore.retrieve("4"))); + assertThat(list.get(2), is((Object) dataStore.retrieve("3"))); + assertThat(list.get(3), is((Object) dataStore.retrieve("2"))); + } + + @Test + public void testFindByExampleWithSortingAndPaging() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("Teheran"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.regex()).withIgnoreCase().withIgnorePaths("address.state.name"); + final Example example = Example.of(probe, matching); + final Iterable found = executor.findAll(example, new PageRequest(2, 1, new Sort(Sort.Direction.ASC, "firstName", "lastName", "address.state.name"))); + assertThat(found, is(notNullValue())); + final List list = TestUtils.iterableToList(found); + assertThat(list, hasSize(1)); + assertThat(list.get(0), is((Object) dataStore.retrieve("3"))); + } + + @Test + public void testCount() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("ehe"))); + final ExampleMatcher matching = ExampleMatcher.matching().withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.contains()).withIgnoreCase(); + final Example example = Example.of(probe, matching); + final long count = executor.count(example); + assertThat(count, is(2L)); + } + + @Test + public void testExists() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("ehe"))); + final ExampleMatcher matching = ExampleMatcher.matching() + .withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.contains()); + final Example example = Example.of(probe, matching); + final boolean exists = executor.exists(example); + assertThat(exists, is(true)); + } + + @Test + public void testWithNullsKept() throws Exception { + final Person probe = new Person().setAddress(new Address().setState(new State().setName("ehe"))); + final ExampleMatcher matching = ExampleMatcher.matching() + .withMatcher("address.state.name", ExampleMatcher.GenericPropertyMatchers.contains()) + .withIncludeNullValues(); + final Example example = Example.of(probe, matching); + final boolean exists = executor.exists(example); + assertThat(exists, is(false)); + } + +} \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtilsTest.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtilsTest.java new file mode 100644 index 00000000..00a4ac2f --- /dev/null +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/repository/PagingAndSortingUtilsTest.java @@ -0,0 +1,16 @@ +package com.mmnaseri.utils.spring.data.repository; + +import com.mmnaseri.utils.spring.data.tools.AbstractUtilityClassTest; + +/** + * @author Milad Naseri (milad.naseri@cdk.com) + * @since 1.0 (6/9/16, 4:06 AM) + */ +public class PagingAndSortingUtilsTest extends AbstractUtilityClassTest { + + @Override + protected Class getUtilityClass() { + return PagingAndSortingUtils.class; + } + +} \ No newline at end of file diff --git a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpQueryDescriptionExtractor.java b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpMethodQueryDescriptionExtractor.java similarity index 69% rename from spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpQueryDescriptionExtractor.java rename to spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpMethodQueryDescriptionExtractor.java index 79b1ffdf..a5bad22c 100644 --- a/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpQueryDescriptionExtractor.java +++ b/spring-data-mock/src/test/java/com/mmnaseri/utils/spring/data/sample/usecases/proxy/resolvers/NoOpMethodQueryDescriptionExtractor.java @@ -1,7 +1,7 @@ package com.mmnaseri.utils.spring.data.sample.usecases.proxy.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.proxy.RepositoryFactoryConfiguration; import com.mmnaseri.utils.spring.data.query.QueryDescriptor; @@ -11,17 +11,17 @@ * @author Milad Naseri (mmnaseri@programmer.net) * @since 1.0 (4/12/16, 6:32 PM) */ -public class NoOpQueryDescriptionExtractor extends QueryDescriptionExtractor { +public class NoOpMethodQueryDescriptionExtractor extends MethodQueryDescriptionExtractor { private boolean called; - public NoOpQueryDescriptionExtractor() { + public NoOpMethodQueryDescriptionExtractor() { super(null); called = false; } @Override - public QueryDescriptor extract(RepositoryMetadata repositoryMetadata, Method method, RepositoryFactoryConfiguration configuration) { + public QueryDescriptor extract(RepositoryMetadata repositoryMetadata, RepositoryFactoryConfiguration configuration, Method method) { called = true; return null; } From 1ea8fb8a6c096b62df7d262bae009565ba0acd4a Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Thu, 9 Jun 2016 04:19:47 -0700 Subject: [PATCH 09/10] chore(travis): update sudo to prepare for release v1.1 Closes #60 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e5f5b785..a69d9b15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ after_success: - '[[ $TRAVIS_BRANCH == "master" && ( "x${TRAVIS_PULL_REQUEST}" == "xfalse" || "x${TRAVIS_PULL_REQUEST}" == "x" ) && ( "x$(echo $JAVA_HOME | grep -o 8)" == "x8" ) ]] && sudo apt-get install gnupg2' - '[[ $TRAVIS_BRANCH == "master" && ( "x${TRAVIS_PULL_REQUEST}" == "xfalse" || "x${TRAVIS_PULL_REQUEST}" == "x" ) && ( "x$(echo $JAVA_HOME | grep -o 8)" == "x8" ) ]] && bash ../deployment/deploy.sh eb1a6f34f056 ../deployment/key.asc.enc ../deployment/settings.xml' # SUDO should be set to `false` except when deploying to OSSRH to trigger container infrastructure -sudo: false +sudo: true env: global: - secure: DPkao3yJ4hhEsUOfs2VQJ8WCV3VdR3ToFiGB1l461PUstjaytTeXYK3bW2PmZqYJs6Hxxwyl8UHFvgBvWLb/yQV/dqyyJAV8XRb9UC37e4ddLoi8HE7NSGQIiXq0tySiMaOTyd2NtRTepfNAMEMDP746Nrox1giPEq1FPv+K98o= From 09784d74e4fb583015f3c10bd525d7e75175e33d Mon Sep 17 00:00:00 2001 From: Milad Naseri Date: Thu, 9 Jun 2016 04:23:47 -0700 Subject: [PATCH 10/10] chore(maven): update artifact version Closes #60 --- spring-data-mock-build/pom.xml | 2 +- spring-data-mock/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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.utils spring-data-mock-build - 1.0.2 + 1.1 Spring Data Mock: Build Aggregator This is the build module that will aggregate all reactors for the spring-data-mock project https://mmnaseri.github.io/spring-data-mock diff --git a/spring-data-mock/pom.xml b/spring-data-mock/pom.xml index eb57a560..63c85392 100644 --- a/spring-data-mock/pom.xml +++ b/spring-data-mock/pom.xml @@ -26,7 +26,7 @@ com.mmnaseri.utils spring-data-mock - 1.0.3 + 1.1 Spring Data Mock A framework for mocking Spring Data repositories https://mmnaseri.github.io/spring-data-mock