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.
This commit is contained in:
Oliver Gierke
2015-04-07 17:01:14 +02:00
parent e741f90378
commit 1bb228df17
7 changed files with 124 additions and 29 deletions

View File

@@ -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);
}
/*

View File

@@ -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<Object> executeSearch(RootResourceInformation resourceInformation, WebRequest request,
@PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) {
public ResponseEntity<Object> executeSearch(RootResourceInformation resourceInformation,
@RequestParam MultiValueMap<String, Object> 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<Object>(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<String, Object> 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<Link> links = new ArrayList<Link>();
@@ -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<String, Object> parameters, Method method, DefaultedPageable pageable, Sort sort,
PersistentEntityResourceAssembler assembler) {
Map<String, String[]> parameters = request.getParameterMap();
return invoker.invokeQueryMethod(method, parameters, pageable.getPageable(), sort);
MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(parameters);
MethodParameters methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class));
for (Entry<String, List<Object>> 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<Object> prepareUris(List<Object> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyList();
}
List<Object> result = new ArrayList<Object>(source.size());
for (Object element : source) {
try {
result.add(new URI(element.toString()));
} catch (URISyntaxException o_O) {
result.add(element);
}
}
return result;
}
}

View File

@@ -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);
}
/**

View File

@@ -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<String, Object> parameters = new LinkedMultiValueMap<String, Object>(1);
parameters.add("firstname", "John");
ResponseEntity<Object> response = controller.executeSearch(resourceInformation, getRequest(parameters),
"firstname", new DefaultedPageable(new PageRequest(0, 10), true), null, assembler);
ResponseEntity<Object> response = controller.executeSearch(resourceInformation, parameters, "firstname", PAGEABLE,
null, assembler);
ResourceTester tester = ResourceTester.of(response.getBody());
PagedResources<Object> pagedResources = tester.assertIsPage();
@@ -169,4 +176,21 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
assertAllowHeaders(response, HttpMethod.GET);
}
/**
* @see DATAREST-502
*/
@Test
public void interpretsUriAsReferenceToRelatedEntity() {
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>(1);
parameters.add("author", "/author/1");
RootResourceInformation resourceInformation = getResourceInformation(Book.class);
ResponseEntity<Object> result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains",
PAGEABLE, null, assembler);
assertThat(result.getBody(), is(instanceOf(Resources.class)));
}
}

View File

@@ -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 {

View File

@@ -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<Book, Long> {
@RestResource(rel = "find-by-sorted")
List<Book> findBy(Sort sort);
@Query("select b from Book b where :author member of b.authors")
List<Book> findByAuthorsContains(@Param("author") Author author);
}

View File

@@ -132,8 +132,6 @@ public class PersistentEntityToJsonSchemaConverterUnitTests {
String writeSchemaFor = writeSchemaFor(type);
System.out.println(writeSchemaFor);
for (Constraint constraint : constraints) {
try {