diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 6a2e96972..c63f85b29 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -18,6 +18,7 @@ package org.springframework.data.rest.webmvc; import static org.springframework.data.rest.core.support.DomainObjectMerger.NullHandlingPolicy.*; import static org.springframework.http.HttpMethod.*; +import java.io.Serializable; import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -44,6 +45,7 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.core.support.DomainObjectMerger; import org.springframework.data.rest.core.support.DomainObjectMerger.NullHandlingPolicy; +import org.springframework.data.rest.webmvc.support.BackendId; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; @@ -56,7 +58,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; -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.ResponseBody; @@ -187,7 +188,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity> getSingleEntity(RootResourceInformation resourceInformation, - @PathVariable String id) throws HttpRequestMethodNotSupportedException { + @BackendId Serializable id) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM); @@ -217,7 +218,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) public ResponseEntity putEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException { + PersistentEntityResource payload, @BackendId Serializable id) + throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -247,8 +249,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH) public ResponseEntity patchEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id) throws HttpRequestMethodNotSupportedException, - ResourceNotFoundException { + PersistentEntityResource payload, @BackendId Serializable id) + throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); @@ -271,7 +273,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @throws HttpRequestMethodNotSupportedException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE) - public ResponseEntity deleteEntity(final RootResourceInformation resourceInformation, @PathVariable final String id) + public ResponseEntity deleteEntity(final RootResourceInformation resourceInformation, @BackendId Serializable id) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.DELETE, ResourceType.ITEM); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index b6fa8944c..24cf2904d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -18,6 +18,7 @@ package org.springframework.data.rest.webmvc; import static org.springframework.data.rest.webmvc.ControllerUtils.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -44,6 +45,7 @@ import org.springframework.data.rest.core.invoke.RepositoryInvoker; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.util.Function; +import org.springframework.data.rest.webmvc.support.BackendId; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; @@ -102,7 +104,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public ResponseEntity followPropertyReference(final RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property) throws Exception { + @BackendId Serializable id, @PathVariable String property) throws Exception { final HttpHeaders headers = new HttpHeaders(); @@ -150,7 +152,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = RequestMethod.DELETE) public ResponseEntity deletePropertyReference(final RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property) throws Exception { + @BackendId Serializable id, @PathVariable String property) throws Exception { final RepositoryInvoker repoMethodInvoker = repoRequest.getInvoker(); @@ -190,7 +192,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.GET) public ResponseEntity followPropertyReference(final RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property, final @PathVariable String propertyId) throws Exception { + @BackendId Serializable id, @PathVariable String property, final @PathVariable String propertyId) + throws Exception { final HttpHeaders headers = new HttpHeaders(); @@ -242,7 +245,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) public ResponseEntity followPropertyReferenceCompact(RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property) throws Exception { + @BackendId Serializable id, @PathVariable String property) throws Exception { ResponseEntity response = followPropertyReference(repoRequest, id, property); @@ -293,7 +296,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @ResponseBody public ResponseEntity createPropertyReference( final RootResourceInformation resourceInformation, final HttpMethod requestMethod, - final @RequestBody Resources incoming, @PathVariable String id, @PathVariable String property) + final @RequestBody Resources incoming, @BackendId Serializable id, @PathVariable String property) throws Exception { final RepositoryInvoker invoker = resourceInformation.getInvoker(); @@ -371,7 +374,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.DELETE) @ResponseBody public ResponseEntity deletePropertyReferenceId(final RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property, final @PathVariable String propertyId) throws Exception { + @BackendId Serializable id, @PathVariable String property, final @PathVariable String propertyId) + throws Exception { final RepositoryInvoker invoker = repoRequest.getInvoker(); @@ -433,8 +437,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro return conversionService.convert(id, type); } - private ResourceSupport doWithReferencedProperty(RootResourceInformation repoRequest, String id, String propertyPath, - Function handler, HttpMethod method) throws Exception { + private ResourceSupport doWithReferencedProperty(RootResourceInformation repoRequest, Serializable id, + String propertyPath, Function handler, HttpMethod method) throws Exception { RepositoryInvoker invoker = repoRequest.getInvoker(); 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 12f340ae2..2066fb771 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 @@ -59,6 +59,7 @@ import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module; import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; @@ -247,6 +248,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new ResourceMetadataHandlerMethodArgumentResolver(repositories(), resourceMappings()); } + @Bean + public BackendIdHandlerMethodArgumentResolver backendIdHandlerMethodArgumentResolver() { + return new BackendIdHandlerMethodArgumentResolver(resourceMetadataHandlerMethodArgumentResolver()); + } + /** * A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current * configuration into account when generating links. @@ -481,7 +487,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon private List defaultMethodArgumentResolvers() { return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), - resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE); + resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, + backendIdHandlerMethodArgumentResolver()); } private ObjectMapper basicObjectMapper() { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendId.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendId.java new file mode 100644 index 000000000..ee3139361 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendId.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 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.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to bind the backend id of an entity. + * + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface BackendId { + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..6e8aa329e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 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.support; + +import java.io.Serializable; + +import org.springframework.core.MethodParameter; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.ResourceMetadataHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.util.UriUtils; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to resolve entity ids for injection int handler method arguments annotated with + * {@link BackendId}. + * + * @author Oliver Gierke + */ +public class BackendIdHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver; + + /** + * Creates a new {@link BackendIdHandlerMethodArgumentResolver} for the given {@link BackendIdConverter}s and + * {@link ResourceMetadataHandlerMethodArgumentResolver}. + * + * @param resourceMetadataResolver the resolver to obtain {@link ResourceMetadata} from. + */ + public BackendIdHandlerMethodArgumentResolver(ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver) { + + Assert.notNull(resourceMetadataResolver, "ResourceMetadata resolver must not be null!"); + + this.resourceMetadataResolver = resourceMetadataResolver; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(BackendId.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + @Override + public String resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { + + Class parameterType = parameter.getParameterType(); + + if (!parameterType.equals(Serializable.class)) { + throw new IllegalArgumentException(String.format( + "Method parameter for @%s must be of type %s! Got %s for method %s.", BackendId.class.getSimpleName(), + Serializable.class.getSimpleName(), parameterType.getSimpleName(), parameter.getMethod())); + } + + ResourceMetadata metadata = resourceMetadataResolver.resolveArgument(parameter, mavContainer, request, + binderFactory); + + if (metadata == null) { + throw new IllegalArgumentException("Could not obtain ResourceMetadata for request " + request); + } + + return UriUtils.findMappingVariable("id", parameter, request); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java new file mode 100644 index 000000000..28b39912e --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014 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.util; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.UriTemplate; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.util.UrlPathHelper; + +/** + * Utility methods to work with requests and URIs. + * + * @author Oliver Gierke + */ +public abstract class UriUtils { + + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + private UriUtils() {} + + /** + * Returns the value for the mapping variable with the given name. + * + * @param variable must not be {@literal null} or empty. + * @param parameter + * @param request + * @return + */ + public static String findMappingVariable(String variable, MethodParameter parameter, NativeWebRequest request) { + + Assert.hasText(variable, "Variable name must not be null or empty!"); + Assert.notNull(parameter, "Method parameter must not be null!"); + Assert.notNull(request, "Request must not be null!"); + + String lookupPath = getCleanLookupPath(request); + RequestMapping annotation = parameter.getMethodAnnotation(RequestMapping.class); + + for (String mapping : annotation.value()) { + + Map variables = new org.springframework.web.util.UriTemplate(mapping).match(lookupPath); + String value = variables.get(variable); + + if (value != null) { + return value; + } + } + + return null; + } + + private static String getCleanLookupPath(NativeWebRequest request) { + + HttpServletRequest httpServletRequest = request.getNativeRequest(HttpServletRequest.class); + String lookupPath = URL_PATH_HELPER.getLookupPathForRequest(httpServletRequest); + return new UriTemplate(lookupPath).expand().toString(); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/BackendIdConverterHandlerMethodArgumentResolverIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/BackendIdConverterHandlerMethodArgumentResolverIntegrationTests.java new file mode 100644 index 000000000..e3a3123a5 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/BackendIdConverterHandlerMethodArgumentResolverIntegrationTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; +import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Integration tests for {@link BackendIdHandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + */ +@ContextConfiguration(classes = JpaRepositoryConfig.class) +public class BackendIdConverterHandlerMethodArgumentResolverIntegrationTests extends AbstractControllerIntegrationTests { + + @Autowired BackendIdHandlerMethodArgumentResolver resolver; + + /** + * @see DATAREST-267, DATAREST-268 + */ + @Test + public void stripsUriTemplateVariablesFromUri() throws Exception { + + Method method = ReflectionUtils.findMethod(SampleController.class, "resolveId", Serializable.class); + MethodParameter parameter = new MethodParameter(method, 0); + NativeWebRequest request = new ServletWebRequest(new MockHttpServletRequest("GET", "/books/5{?projection}")); + + Object resolvedId = resolver.resolveArgument(parameter, null, request, null); + + assertThat(resolvedId, is((Object) "5")); + } + + static class SampleController { + + @RequestMapping("/{repository}/{id}") + void resolveId(@BackendId Serializable backendId) {} + } +}