DATAREST-712 - Fixed application of Querydsl bindings to only kick in for collection resources.

The Querydsl integration in RootResourceInformationHandlerMethodArgumentResolver now only kicks in if the controller method parameter is annotated with @QuerydslPredicate. This allows us to apply the QuerydslBindings for collection resources but make sure they aren't accidentally applied during executions of query methods etc.

Improved the type lookup for query method execution to make sure the correct mapping is looked up for collection parameters.
This commit is contained in:
Oliver Gierke
2015-11-24 15:35:39 +01:00
parent 8329fcf8f6
commit b663b5ff73
8 changed files with 70 additions and 28 deletions

View File

@@ -114,6 +114,13 @@
<version>${springdata.mongodb}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-mongodb</artifactId>
<version>${querydsl}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>

View File

@@ -34,6 +34,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.querydsl.binding.QuerydslPredicate;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
@@ -190,8 +191,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
*/
@ResponseBody
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET)
public Resources<?> getCollectionResource(RootResourceInformation resourceInformation, DefaultedPageable pageable,
Sort sort, PersistentEntityResourceAssembler assembler)
public Resources<?> getCollectionResource(@QuerydslPredicate RootResourceInformation resourceInformation,
DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION);
@@ -202,13 +203,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
throw new ResourceNotFoundException();
}
Iterable<?> results;
if (pageable.getPageable() != null) {
results = invoker.invokeFindAll(pageable.getPageable());
} else {
results = invoker.invokeFindAll(sort);
}
Iterable<?> results = pageable.getPageable() != null ? invoker.invokeFindAll(pageable.getPageable())
: invoker.invokeFindAll(sort);
ResourceMetadata metadata = resourceInformation.getResourceMetadata();
Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(),
@@ -226,7 +222,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
SearchResourceMappings searchMappings = metadata.getSearchResourceMappings();
List<Link> links = new ArrayList<Link>();
links.add(new Link(ProfileController.getPath(this.config, metadata), ProfileResourceProcessor.PROFILE_REL));
if (searchMappings.isExported()) {
@@ -241,16 +236,16 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@SuppressWarnings({ "unchecked" })
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET,
produces = { "application/x-spring-data-compact+json", "text/uri-list" })
public Resources<?> getCollectionResourceCompact(RootResourceInformation repoRequest, DefaultedPageable pageable,
Sort sort, PersistentEntityResourceAssembler assembler)
public Resources<?> getCollectionResourceCompact(@QuerydslPredicate RootResourceInformation resourceinformation,
DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
Resources<?> resources = getCollectionResource(repoRequest, pageable, sort, assembler);
Resources<?> resources = getCollectionResource(resourceinformation, pageable, sort, assembler);
List<Link> links = new ArrayList<Link>(resources.getLinks());
for (Resource<?> resource : ((Resources<Resource<?>>) resources).getContent()) {
PersistentEntityResource persistentEntityResource = (PersistentEntityResource) resource;
links.add(resourceLink(repoRequest, persistentEntityResource));
links.add(resourceLink(resourceinformation, persistentEntityResource));
}
if (resources instanceof PagedResources) {
return new PagedResources<Object>(Collections.emptyList(), ((PagedResources<?>) resources).getMetadata(), links);

View File

@@ -37,6 +37,8 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
@@ -294,6 +296,9 @@ class RepositorySearchController extends AbstractRepositoryRestController {
MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(parameters);
MethodParameters methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class));
List<MethodParameter> parameterList = methodParameters.getParameters();
List<TypeInformation<?>> parameterTypeInformations = ClassTypeInformation.from(method.getDeclaringClass())
.getParameterTypes(method);
for (Entry<String, List<Object>> entry : parameters.entrySet()) {
@@ -303,7 +308,10 @@ class RepositorySearchController extends AbstractRepositoryRestController {
continue;
}
ResourceMetadata metadata = mappings.getMetadataFor(parameter.getParameterType());
int parameterIndex = parameterList.indexOf(parameter);
TypeInformation<?> domainType = parameterTypeInformations.get(parameterIndex).getActualType();
ResourceMetadata metadata = mappings.getMetadataFor(domainType.getType());
if (metadata != null && metadata.isExported()) {
result.put(parameter.getParameterName(), prepareUris(entry.getValue()));

View File

@@ -18,10 +18,12 @@ package org.springframework.data.rest.webmvc.config;
import java.util.Arrays;
import java.util.Map;
import org.springframework.core.MethodParameter;
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.QuerydslPredicate;
import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvoker;
@@ -73,12 +75,13 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
*/
@Override
@SuppressWarnings({ "unchecked" })
protected RepositoryInvoker postProcess(RepositoryInvoker invoker, Class<?> domainType,
Map<String, String[]> parameters) {
protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker,
Class<?> domainType, Map<String, String[]> parameters) {
Object repository = repositories.getRepositoryFor(domainType);
if (!QueryDslPredicateExecutor.class.isInstance(repository)) {
if (!QueryDslPredicateExecutor.class.isInstance(repository)
|| !parameter.hasParameterAnnotation(QuerydslPredicate.class)) {
return invoker;
}

View File

@@ -89,19 +89,20 @@ public class RootResourceInformationHandlerMethodArgumentResolver implements Han
// TODO reject if ResourceMetadata cannot be resolved
return new RootResourceInformation(resourceMetadata, persistentEntity,
postProcess(repositoryInvoker, domainType, webRequest.getParameterMap()));
postProcess(parameter, 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 parameter must not be {@literal null}.
* @param invoker will never be {@literal null}.
* @param domainType will never be {@literal null}.
* @param parameters will never be {@literal null}.
* @return
* @return the post-processed {@link RepositoryInvoker}.
*/
protected RepositoryInvoker postProcess(RepositoryInvoker invoker, Class<?> domainType,
protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker, Class<?> domainType,
Map<String, String[]> parameters) {
return invoker;
}

View File

@@ -28,6 +28,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter;
@@ -35,13 +36,15 @@ 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.QuerydslPredicate;
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.Receipt;
import org.springframework.data.rest.webmvc.mongodb.ReceiptRepository;
import org.springframework.data.rest.webmvc.mongodb.User;
import org.springframework.data.rest.webmvc.mongodb.UserRepository;
import org.springframework.test.util.ReflectionTestUtils;
/**
@@ -59,6 +62,7 @@ public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUn
@Mock ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver;
@Mock RepositoryInvoker invoker;
@Mock MethodParameter parameter;
QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver resolver;
@@ -72,6 +76,8 @@ public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUn
this.resolver = new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories, invokerFactory,
resourceMetadataResolver, builder, factory);
when(parameter.hasParameterAnnotation(QuerydslPredicate.class)).thenReturn(true);
}
/**
@@ -80,10 +86,10 @@ public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUn
@Test
public void returnsInvokerIfRepositoryIsNotQuerydslAware() {
UserRepository repository = mock(UserRepository.class);
when(repositories.getRepositoryFor(User.class)).thenReturn(repository);
ReceiptRepository repository = mock(ReceiptRepository.class);
when(repositories.getRepositoryFor(Receipt.class)).thenReturn(repository);
RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS);
RepositoryInvoker result = resolver.postProcess(parameter, invoker, Receipt.class, NO_PARAMETERS);
assertThat(result, is(invoker));
}
@@ -97,7 +103,7 @@ public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUn
Object repository = mock(QuerydslUserRepository.class);
when(repositories.getRepositoryFor(User.class)).thenReturn(repository);
RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS);
RepositoryInvoker result = resolver.postProcess(parameter, invoker, User.class, NO_PARAMETERS);
assertThat(result, is(instanceOf(QuerydslRepositoryInvokerAdapter.class)));
}
@@ -112,7 +118,7 @@ public class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUn
when(repositories.hasRepositoryFor(User.class)).thenReturn(true);
when(repositories.getRepositoryFor(User.class)).thenReturn(repository);
RepositoryInvoker result = resolver.postProcess(invoker, User.class, NO_PARAMETERS);
RepositoryInvoker result = resolver.postProcess(parameter, invoker, User.class, NO_PARAMETERS);
assertThat(result, is(instanceOf(QuerydslRepositoryInvokerAdapter.class)));
verify(repository, times(1)).customize(Mockito.any(QuerydslBindings.class), Mockito.any(QUser.class));

View File

@@ -31,6 +31,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.CommonWebTests;
import org.springframework.data.rest.webmvc.RestMediaTypes;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -52,6 +53,7 @@ public class MongoWebTests extends CommonWebTests {
@Autowired ProfileRepository repository;
@Autowired UserRepository userRepository;
@Autowired RepositoryEntityLinks entityLinks;
ObjectMapper mapper = new ObjectMapper();
@@ -327,4 +329,20 @@ public class MongoWebTests extends CommonWebTests {
mvc.perform(get(link.expand("").getHref())).//
andExpect(status().isNotFound());
}
/**
* @see DATAREST-712
*/
@Test
public void invokesQueryMethodTakingAReferenceCorrectly() throws Exception {
Link link = client.discoverUnique("users", "search", "findByColleaguesContains");
User thomas = userRepository.findAll(QUser.user.firstname.eq("Thomas")).iterator().next();
Link thomasUri = entityLinks.linkToSingleResource(User.class, thomas.id).expand();
String href = link.expand(thomasUri.getHref()).getHref();
mvc.perform(get(href)).andExpect(status().isOk());
}
}

View File

@@ -18,12 +18,16 @@ package org.springframework.data.rest.webmvc.mongodb;
import java.math.BigInteger;
import java.util.List;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
/**
* @author Oliver Gierke
*/
public interface UserRepository extends CrudRepository<User, BigInteger> {
public interface UserRepository extends CrudRepository<User, BigInteger>, QueryDslPredicateExecutor<User> {
List<User> findByFirstname(String firstname);
List<User> findByColleaguesContains(@Param("colleagues") User colleague);
}