From 1bb228df172ad3bea7c5cff3a41db72ff5eefaed Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 7 Apr 2015 17:01:14 +0200 Subject: [PATCH] DATAREST-502 - Prepare request parameters to enable URI-to-entity resolution for executing searches. RepositorySearchController now takes all request parameters as MultiValueMap to execute query methods. For parameters mapping to a managed resource we try to interpret the given parameter value as URI to actually trigger the URI-to-entity resolution through the newly registered UriToEntityConverter. The latter is now registered with the default ConversionService registered so that it's A lot of cleanups in RepositorySearchController. Removed some obsolete prints to the console to reduce log output during test execution. Related ticket: DATACMNS-678. --- .../data/rest/core/UriToEntityConverter.java | 2 +- .../webmvc/RepositorySearchController.java | 94 +++++++++++++++---- .../RepositoryRestMvcConfiguration.java | 15 ++- ...itorySearchControllerIntegrationTests.java | 30 +++++- .../alps/AlpsControllerIntegrationTests.java | 5 +- .../data/rest/webmvc/jpa/BookRepository.java | 5 + ...tEntityToJsonSchemaConverterUnitTests.java | 2 - 7 files changed, 124 insertions(+), 29 deletions(-) diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java index eb9f32602..b4e74297a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java @@ -81,7 +81,7 @@ public class UriToEntityConverter implements ConditionalGenericConverter { */ @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - return conversionService.canConvert(STRING_TYPE, targetType); + return !sourceType.equals(URI_TYPE) ? false : conversionService.canConvert(STRING_TYPE, targetType); } /* diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index e3ec5c7e5..857319a52 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -18,16 +18,21 @@ package org.springframework.data.rest.webmvc; import static org.springframework.data.rest.webmvc.ControllerUtils.*; import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Map.Entry; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; import org.springframework.data.auditing.AuditableBeanWrapperFactory; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.core.mapping.ResourceMappings; +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; @@ -39,17 +44,21 @@ import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.AnnotationAttribute; +import org.springframework.hateoas.core.MethodParameters; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.context.request.WebRequest; /** * Controller to lookup and execute searches on a given repository. @@ -153,20 +162,23 @@ class RepositorySearchController extends AbstractRepositoryRestController { /** * Executes the search with the given name. * - * @param request - * @param repository + * @param resourceInformation + * @param parameters * @param search * @param pageable + * @param sort + * @param assembler * @return * @throws ResourceNotFoundException */ @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET) - public ResponseEntity executeSearch(RootResourceInformation resourceInformation, WebRequest request, - @PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { + public ResponseEntity executeSearch(RootResourceInformation resourceInformation, + @RequestParam MultiValueMap parameters, @PathVariable String search, DefaultedPageable pageable, + Sort sort, PersistentEntityResourceAssembler assembler) { Method method = checkExecutability(resourceInformation, search); - Object result = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler); + Object result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, assembler); return new ResponseEntity(toResource(result, assembler, null), HttpStatus.OK); } @@ -175,20 +187,23 @@ class RepositorySearchController extends AbstractRepositoryRestController { * Executes a query method and exposes the results in compact form. * * @param resourceInformation + * @param parameters * @param repository - * @param method + * @param searcg * @param pageable + * @param sort + * @param assembler * @return */ @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET, // produces = { "application/x-spring-data-compact+json" }) - public ResourceSupport executeSearchCompact(RootResourceInformation resourceInformation, WebRequest request, - @PathVariable String repository, @PathVariable String search, DefaultedPageable pageable, Sort sort, - PersistentEntityResourceAssembler assembler) { + public ResourceSupport executeSearchCompact(RootResourceInformation resourceInformation, + @RequestParam MultiValueMap parameters, @PathVariable String repository, + @PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { Method method = checkExecutability(resourceInformation, search); - Object result = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler); + Object result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, assembler); Object resource = toResource(result, assembler, null); List links = new ArrayList(); @@ -272,11 +287,29 @@ class RepositorySearchController extends AbstractRepositoryRestController { * @param pageable * @return */ - private Object executeQueryMethod(final RepositoryInvoker invoker, WebRequest request, Method method, - DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { + private Object executeQueryMethod(final RepositoryInvoker invoker, + @RequestParam MultiValueMap parameters, Method method, DefaultedPageable pageable, Sort sort, + PersistentEntityResourceAssembler assembler) { - Map parameters = request.getParameterMap(); - return invoker.invokeQueryMethod(method, parameters, pageable.getPageable(), sort); + MultiValueMap result = new LinkedMultiValueMap(parameters); + MethodParameters methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class)); + + for (Entry> entry : parameters.entrySet()) { + + MethodParameter parameter = methodParameters.getParameter(entry.getKey()); + + if (parameter == null) { + continue; + } + + ResourceMetadata metadata = mappings.getMappingFor(parameter.getParameterType()); + + if (metadata != null && metadata.isExported()) { + result.put(parameter.getParameterName(), prepareUris(entry.getValue())); + } + } + + return invoker.invokeQueryMethod(method, result, pageable.getPageable(), sort); } /** @@ -284,7 +317,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { * * @param resourceInformation */ - private SearchResourceMappings verifySearchesExposed(RootResourceInformation resourceInformation) { + private static SearchResourceMappings verifySearchesExposed(RootResourceInformation resourceInformation) { SearchResourceMappings resourceMappings = resourceInformation.getSearchMappings(); @@ -294,4 +327,31 @@ class RepositorySearchController extends AbstractRepositoryRestController { return resourceMappings; } + + /** + * Tries to turn all elements of the given {@link List} into URIs and falls back to keeping the original element if + * the conversion fails. + * + * @param source can be {@literal null}. + * @return + */ + private static List prepareUris(List source) { + + if (source == null || source.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList(source.size()); + + for (Object element : source) { + + try { + result.add(new URI(element.toString())); + } catch (URISyntaxException o_O) { + result.add(element); + } + } + + 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 2e5e11577..d1d4acc2d 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 @@ -40,6 +40,7 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.data.auditing.AuditableBeanWrapperFactory; @@ -181,8 +182,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon public DefaultFormattingConversionService defaultConversionService() { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); - // Add Spring Data Commons formatters + conversionService.addConverter(uriToEntityConverter(conversionService)); addFormatters(conversionService); configureConversionService(conversionService); @@ -540,8 +541,16 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon PersistentEntities entities = persistentEntities(); - return new PersistentEntityJackson2Module(resourceMappings(), entities, config(), new UriToEntityConverter( - entities, defaultConversionService())); + return new PersistentEntityJackson2Module(resourceMappings(), entities, config(), + uriToEntityConverter(defaultConversionService())); + } + + /** + * @param entities + * @return + */ + protected UriToEntityConverter uriToEntityConverter(ConversionService conversionService) { + return new UriToEntityConverter(persistentEntities(), conversionService); } /** diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index 2c182c675..7d523f9dc 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -27,6 +27,7 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.ResourceTester.HasSelfLink; import org.springframework.data.rest.webmvc.jpa.Address; import org.springframework.data.rest.webmvc.jpa.Author; +import org.springframework.data.rest.webmvc.jpa.Book; import org.springframework.data.rest.webmvc.jpa.CreditCard; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Person; @@ -34,11 +35,14 @@ import org.springframework.data.rest.webmvc.jpa.TestDataPopulator; import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.Resources; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Integration tests for the {@link RepositorySearchController}. @@ -49,6 +53,8 @@ import org.springframework.transaction.annotation.Transactional; @Transactional public class RepositorySearchControllerIntegrationTests extends AbstractControllerIntegrationTests { + static final DefaultedPageable PAGEABLE = new DefaultedPageable(new PageRequest(0, 10), true); + @Autowired TestDataPopulator loader; @Autowired RepositorySearchController controller; @Autowired PersistentEntityResourceAssembler assembler; @@ -88,11 +94,12 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll @Test public void executesSearchAgainstRepository() { - RequestParameters parameters = new RequestParameters("firstname", "John"); RootResourceInformation resourceInformation = getResourceInformation(Person.class); + MultiValueMap parameters = new LinkedMultiValueMap(1); + parameters.add("firstname", "John"); - ResponseEntity response = controller.executeSearch(resourceInformation, getRequest(parameters), - "firstname", new DefaultedPageable(new PageRequest(0, 10), true), null, assembler); + ResponseEntity response = controller.executeSearch(resourceInformation, parameters, "firstname", PAGEABLE, + null, assembler); ResourceTester tester = ResourceTester.of(response.getBody()); PagedResources pagedResources = tester.assertIsPage(); @@ -169,4 +176,21 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll assertAllowHeaders(response, HttpMethod.GET); } + + /** + * @see DATAREST-502 + */ + @Test + public void interpretsUriAsReferenceToRelatedEntity() { + + MultiValueMap parameters = new LinkedMultiValueMap(1); + parameters.add("author", "/author/1"); + + RootResourceInformation resourceInformation = getResourceInformation(Book.class); + + ResponseEntity result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains", + PAGEABLE, null, assembler); + + assertThat(result.getBody(), is(instanceOf(Resources.class))); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java index 427103b16..3a453666b 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/alps/AlpsControllerIntegrationTests.java @@ -36,7 +36,6 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -138,8 +137,8 @@ public class AlpsControllerIntegrationTests extends AbstractControllerIntegratio assertThat(usersLink, is(notNullValue())); - mvc.perform(get(usersLink.getHref())).andDo(MockMvcResultHandlers.print()) - .andExpect(jsonPath("$.descriptors[?(@.id == 'item-representation')].href", is(notNullValue()))); + mvc.perform(get(usersLink.getHref())).// + andExpect(jsonPath("$.descriptors[?(@.id == 'item-representation')].href", is(notNullValue()))); } private Link discoverUnique(String href, String rel) throws Exception { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java index 96ad97cd8..43d277a80 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java @@ -18,7 +18,9 @@ package org.springframework.data.rest.webmvc.jpa; import java.util.List; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; @@ -30,4 +32,7 @@ public interface BookRepository extends CrudRepository { @RestResource(rel = "find-by-sorted") List findBy(Sort sort); + + @Query("select b from Book b where :author member of b.authors") + List findByAuthorsContains(@Param("author") Author author); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java index 7f0758bab..a2357fa75 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java @@ -132,8 +132,6 @@ public class PersistentEntityToJsonSchemaConverterUnitTests { String writeSchemaFor = writeSchemaFor(type); - System.out.println(writeSchemaFor); - for (Constraint constraint : constraints) { try {