diff --git a/spring-data-rest-webmvc/pom.xml b/spring-data-rest-webmvc/pom.xml index 10bb4cd11..4e31622bb 100644 --- a/spring-data-rest-webmvc/pom.xml +++ b/spring-data-rest-webmvc/pom.xml @@ -88,6 +88,15 @@ json-patch 1.7 + + + + + com.mysema.querydsl + querydsl-core + ${querydsl} + true + @@ -450,6 +459,33 @@ + + + com.mysema.maven + apt-maven-plugin + ${apt} + + + com.mysema.querydsl + querydsl-apt + ${querydsl} + + + + + generate-test-sources + + test-process + + + target/generated-sources/annotations + org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor + true + + + + + diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..722a84b2a --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.config; + +import java.util.Arrays; +import java.util.Map; + +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; +import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.repository.support.RepositoryInvoker; +import org.springframework.data.repository.support.RepositoryInvokerFactory; +import org.springframework.data.rest.webmvc.RootResourceInformation; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +import com.mysema.query.types.Predicate; + +/** + * {@link HandlerMethodArgumentResolver} to create {@link RootResourceInformation} for injection into Spring MVC + * controller methods. + * + * @author Oliver Gierke + * @since 2.4 + */ +class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver + extends RootResourceInformationHandlerMethodArgumentResolver { + + private final Repositories repositories; + private final QuerydslPredicateBuilder predicateBuilder; + private final QuerydslBindingsFactory factory; + + /** + * Creates a new {@link QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver} using the given + * {@link Repositories}, {@link RepositoryInvokerFactory} and {@link ResourceMetadataHandlerMethodArgumentResolver}. + * + * @param repositories must not be {@literal null}. + * @param invokerFactory must not be {@literal null}. + * @param resourceMetadataResolver must not be {@literal null}. + */ + public QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(Repositories repositories, + RepositoryInvokerFactory invokerFactory, ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver, + QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory) { + + super(repositories, invokerFactory, resourceMetadataResolver); + + this.repositories = repositories; + this.predicateBuilder = predicateBuilder; + this.factory = factory; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver#postProcess(org.springframework.data.repository.support.RepositoryInvoker, java.lang.Class, java.util.Map) + */ + @Override + @SuppressWarnings({ "unchecked" }) + protected RepositoryInvoker postProcess(RepositoryInvoker invoker, Class domainType, + Map parameters) { + + Object repository = repositories.getRepositoryFor(domainType); + + if (!QueryDslPredicateExecutor.class.isInstance(repository)) { + return invoker; + } + + ClassTypeInformation type = ClassTypeInformation.from(domainType); + + QuerydslBindings bindings = factory.createBindingsFor(null, type); + Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(parameters), bindings); + + return new QuerydslRepositoryInvokerAdapter(invoker, (QueryDslPredicateExecutor) repository, predicate); + } + + /** + * Converts the given Map into a {@link MultiValueMap}. + * + * @param source must not be {@literal null}. + * @return + */ + private static MultiValueMap toMultiValueMap(Map source) { + + MultiValueMap result = new LinkedMultiValueMap(); + + for (String key : source.keySet()) { + result.put(key, Arrays.asList(source.get(key))); + } + + return result; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 07458405e..e2d70b7ea 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -51,6 +51,9 @@ import org.springframework.data.geo.GeoModule; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.querydsl.QueryDslUtils; +import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; +import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvokerFactory; @@ -97,6 +100,7 @@ import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.util.AnnotatedTypeScanner; import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; +import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration; import org.springframework.data.web.config.SpringDataJacksonConfiguration; import org.springframework.format.support.DefaultFormattingConversionService; @@ -145,7 +149,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; @ComponentScan(basePackageClasses = RepositoryRestController.class, includeFilters = @Filter(BasePathAwareController.class) , useDefaultFilters = false) @ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml") -@Import(SpringDataJacksonConfiguration.class) +@Import({ SpringDataJacksonConfiguration.class, EnableSpringDataWebSupport.QuerydslActivator.class }) public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration implements InitializingBean { private static final boolean IS_JPA_AVAILABLE = ClassUtils.isPresent("javax.persistence.EntityManager", @@ -301,6 +305,17 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver() { + + if (QueryDslUtils.QUERY_DSL_PRESENT) { + + QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class); + QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(defaultConversionService(), + factory.getEntityPathResolver()); + + return new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories(), + repositoryInvokerFactory(), resourceMetadataHandlerMethodArgumentResolver(), predicateBuilder, factory); + } + return new RootResourceInformationHandlerMethodArgumentResolver(repositories(), repositoryInvokerFactory(), resourceMetadataHandlerMethodArgumentResolver()); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java index dc66c1415..2b01d06c4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RootResourceInformationHandlerMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.springframework.data.rest.webmvc.config; +import java.util.Map; + import org.springframework.core.MethodParameter; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.support.Repositories; @@ -86,6 +88,21 @@ public class RootResourceInformationHandlerMethodArgumentResolver implements Han PersistentEntity persistentEntity = repositories.getPersistentEntity(domainType); // TODO reject if ResourceMetadata cannot be resolved - return new RootResourceInformation(resourceMetadata, persistentEntity, repositoryInvoker); + return new RootResourceInformation(resourceMetadata, persistentEntity, + postProcess(repositoryInvoker, domainType, webRequest.getParameterMap())); + } + + /** + * Potentially customize the given {@link RepositoryInvoker} for the given domain type. Default implementations simply + * returns the given invoker as is. + * + * @param invoker will never be {@literal null}. + * @param domainType will never be {@literal null}. + * @param parameters will never be {@literal null}. + * @return + */ + protected RepositoryInvoker postProcess(RepositoryInvoker invoker, Class domainType, + Map parameters) { + return invoker; } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests.java new file mode 100644 index 000000000..bbfa721f8 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.config; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.querydsl.QueryDslPredicateExecutor; +import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; +import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.repository.support.RepositoryInvoker; +import org.springframework.data.repository.support.RepositoryInvokerFactory; +import org.springframework.data.rest.webmvc.mongodb.QUser; +import org.springframework.data.rest.webmvc.mongodb.User; +import org.springframework.data.rest.webmvc.mongodb.UserRepository; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Unit tests for {@link QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests { + + static final Map NO_PARAMETERS = Collections.emptyMap(); + + @Mock Repositories repositories; + @Mock RepositoryInvokerFactory invokerFactory; + @Mock ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver; + + @Mock RepositoryInvoker invoker; + + QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver resolver; + + @Before + public void setUp() { + + QuerydslBindingsFactory factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); + ReflectionTestUtils.setField(factory, "repositories", repositories); + QuerydslPredicateBuilder builder = new QuerydslPredicateBuilder(new DefaultConversionService(), + factory.getEntityPathResolver()); + + this.resolver = new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories, invokerFactory, + resourceMetadataResolver, builder, factory); + } + + /** + * @see DATAREST-616 + */ + @Test + public void returnsInvokerIfRepositoryIsNotQuerydslAware() { + + UserRepository repository = mock(UserRepository.class); + when(repositories.getRepositoryFor(User.class)).thenReturn(repository); + + RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS); + + assertThat(result, is(invoker)); + } + + /** + * @see DATAREST-616 + */ + @Test + public void wrapsInvokerInQuerydslAdapter() { + + Object repository = mock(QuerydslUserRepository.class); + when(repositories.getRepositoryFor(User.class)).thenReturn(repository); + + RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS); + + assertThat(result, is(instanceOf(QuerydslRepositoryInvokerAdapter.class))); + } + + /** + * @see DATAREST-616 + */ + @Test + public void invokesCustomizationOnRepositoryIfItImplementsCustomizer() { + + QuerydslCustomizingUserRepository repository = mock(QuerydslCustomizingUserRepository.class); + when(repositories.hasRepositoryFor(User.class)).thenReturn(true); + when(repositories.getRepositoryFor(User.class)).thenReturn(repository); + + RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS); + + assertThat(result, is(instanceOf(QuerydslRepositoryInvokerAdapter.class))); + verify(repository, times(1)).customize(Mockito.any(QuerydslBindings.class), Mockito.any(QUser.class)); + } + + interface QuerydslUserRepository extends QueryDslPredicateExecutor {} + + interface QuerydslCustomizingUserRepository + extends QueryDslPredicateExecutor, QuerydslBinderCustomizer {} +}