diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java deleted file mode 100644 index c7cb057d2..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.springframework.data.rest.core.util; - -import java.net.URI; - -import org.springframework.web.util.UriComponentsBuilder; - -/** - * Helper methods for dealing with URIs. - * - * @author Jon Brisbin - */ -public abstract class UriUtils { - - /** - * Create a new {@link URI} out of the components. - * - * @param baseUri The base URI these path segments are relative to. - * @param pathSegments The path segments to add to the given base URI. - * @return A new URI built from the given base URI and additional path segments. - */ - public static URI buildUri(URI baseUri, String... pathSegments) { - return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); - } - -} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java deleted file mode 100644 index 77b5686a2..000000000 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/util/UriUtilsUnitTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2013 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.core.util; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; - -import java.net.URI; - -import org.junit.Test; - -/** - * Tests to verify that {@link UriUtils} can manipulate {@link URI}s. - * - * @author Jon Brisbin - */ -public class UriUtilsUnitTests { - - private static final String BASE_URI_STR = "http://localhost:8080/data"; - private static final URI BASE_URI = URI.create(BASE_URI_STR); - - private static final String PERSON_2LVL_STR = BASE_URI_STR + "/person/1"; - private static final URI PERSON_2LVL_URI = URI.create(PERSON_2LVL_STR); - - @Test - public void shouldBuildURIFromPathSegments() throws Exception { - - URI uri = UriUtils.buildUri(BASE_URI, "person", "1"); - assertThat(uri, is(PERSON_2LVL_URI)); - } -} 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 656429fa6..7f0ae63c9 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; @@ -168,7 +169,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) - public ResponseEntity postEntity(RootResourceInformation resourceInformation, + public ResponseEntity postCollectionResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { @@ -187,7 +188,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity> getItemResource(RootResourceInformation resourceInformation, - @PathVariable String id, PersistentEntityResourceAssembler assembler) + @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM); @@ -217,8 +218,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @throws HttpRequestMethodNotSupportedException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) - public ResponseEntity putEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id, PersistentEntityResourceAssembler assembler) + public ResponseEntity putItemResource(RootResourceInformation resourceInformation, + PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -248,8 +249,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @throws ResourceNotFoundException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH) - public ResponseEntity patchEntity(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @PathVariable String id, PersistentEntityResourceAssembler assembler) + public ResponseEntity patchItemResource(RootResourceInformation resourceInformation, + PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); @@ -273,7 +274,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 deleteItemResource(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 d46665897..2d2201134 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; @@ -100,7 +102,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public ResponseEntity followPropertyReference(final RootResourceInformation repoRequest, - @PathVariable String id, @PathVariable String property, final PersistentEntityResourceAssembler assembler) + @BackendId Serializable id, @PathVariable String property, final PersistentEntityResourceAssembler assembler) throws Exception { final HttpHeaders headers = new HttpHeaders(); @@ -149,7 +151,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(); @@ -189,7 +191,7 @@ 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, + @BackendId Serializable id, @PathVariable String property, final @PathVariable String propertyId, final PersistentEntityResourceAssembler assembler) throws Exception { final HttpHeaders headers = new HttpHeaders(); @@ -242,7 +244,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, PersistentEntityResourceAssembler assembler) + @BackendId Serializable id, @PathVariable String property, PersistentEntityResourceAssembler assembler) throws Exception { ResponseEntity response = followPropertyReference(repoRequest, id, property, assembler); @@ -294,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(); @@ -372,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(); @@ -434,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 386ebbefe..60a67570f 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 @@ -36,7 +36,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.core.env.Environment; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; @@ -64,6 +63,9 @@ 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.spi.BackendIdConverter; +import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; +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; @@ -86,6 +88,8 @@ import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.plugin.core.OrderAwarePluginRegistry; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; @@ -132,10 +136,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon RepositoryRestMvcConfiguration.class.getClassLoader()); @Autowired ListableBeanFactory beanFactory; - @Autowired Environment environment; - @Autowired(required = false) List> resourceProcessors = Collections.emptyList(); @Autowired(required = false) List> mappingContexts = Collections.emptyList(); + @Autowired(required = false) List> resourceProcessors = Collections.emptyList(); + @Autowired(required = false) List idConverters = Collections.emptyList(); + @Autowired(required = false) RelProvider relProvider; @Autowired(required = false) CurieProvider curieProvider; @@ -270,6 +275,12 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new ResourceMetadataHandlerMethodArgumentResolver(repositories(), resourceMappings()); } + @Bean + public BackendIdHandlerMethodArgumentResolver backendIdHandlerMethodArgumentResolver() { + return new BackendIdHandlerMethodArgumentResolver(backendIdConverterRegistry(), + resourceMetadataHandlerMethodArgumentResolver()); + } + /** * A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current * configuration into account when generating links. @@ -279,7 +290,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public EntityLinks entityLinks() { - return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), pageableResolver()); + return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), pageableResolver(), + backendIdConverterRegistry()); } /** @@ -526,16 +538,25 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return resolver; } + @Bean + public PluginRegistry> backendIdConverterRegistry() { + + List converters = new ArrayList(idConverters.size()); + converters.addAll(this.idConverters); + converters.add(DefaultIdConverter.INSTANCE); + + return OrderAwarePluginRegistry.create(converters); + } + private List defaultMethodArgumentResolvers() { PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver( repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory)); - return Arrays - .asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), - repoRequestArgumentResolver(), persistentEntityArgumentResolver(), - resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, - peraResolver); + return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), + repoRequestArgumentResolver(), persistentEntityArgumentResolver(), + resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, + peraResolver, backendIdHandlerMethodArgumentResolver()); } private ObjectMapper basicObjectMapper() { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java index a374ecd7d..a06534c16 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java @@ -18,19 +18,17 @@ package org.springframework.data.rest.webmvc.config; import static org.springframework.util.ClassUtils.*; import static org.springframework.util.StringUtils.*; -import javax.servlet.http.HttpServletRequest; - import org.springframework.core.MethodParameter; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; +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; -import org.springframework.web.util.UrlPathHelper; /** * {@link HandlerMethodArgumentResolver} to create {@link ResourceMetadata} instances. @@ -76,32 +74,15 @@ public class ResourceMetadataHandlerMethodArgumentResolver implements HandlerMet public ResourceMetadata resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String requestUri = new UrlPathHelper().getLookupPathForRequest(request); + String repositoryKey = UriUtils.findMappingVariable("repository", parameter, webRequest); - if (requestUri.startsWith("/")) { - requestUri = requestUri.substring(1); - } - - String[] parts = requestUri.split("/"); - - if (parts.length == 0) { - // Root request - return null; - } - - return findRepositoryInfoFor(parts[0]); - } - - private ResourceMetadata findRepositoryInfoFor(String pathSegment) { - - if (!hasText(pathSegment)) { + if (!hasText(repositoryKey)) { return null; } for (Class domainType : repositories) { ResourceMetadata mapping = mappings.getMappingFor(domainType); - if (mapping.getPath().matches(pathSegment) && mapping.isExported()) { + if (mapping.getPath().matches(repositoryKey) && mapping.isExported()) { return mapping; } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/spi/BackendIdConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/spi/BackendIdConverter.java new file mode 100644 index 000000000..fe8dccfec --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/spi/BackendIdConverter.java @@ -0,0 +1,83 @@ +/* + * 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.spi; + +import java.io.Serializable; + +import org.springframework.plugin.core.Plugin; + +/** + * SPI to allow the customization of how entity ids are exposed in URIs generated. + * + * @author Oliver Gierke + */ +public interface BackendIdConverter extends Plugin> { + + /** + * Returns the id of the entity to be looked up eventually. + * + * @param id the source id as it was parsed from the incoming request, will never be {@literal null}. + * @param entityType the type of the object to be resolved, will never be {@literal null}. + * @return must not be {@literal null}. + */ + Serializable fromRequestId(String id, Class entityType); + + /** + * Returns the id to be used in the URI generated to point to an entity of the given type with the given id. + * + * @param id the entity's id, will never be {@literal null}. + * @param entityType the type of the entity to expose. + * @return + */ + String toRequestId(Serializable id, Class entityType); + + /** + * The default {@link BackendIdConverter} that will simply use ids as they are. + * + * @author Oliver Gierke + */ + public enum DefaultIdConverter implements BackendIdConverter { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.BackendIdConverter#fromRequestId(java.lang.String, java.lang.Class) + */ + @Override + public Serializable fromRequestId(String id, Class entityType) { + return id; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.BackendIdConverter#toRequestId(java.lang.Object, java.lang.Class) + */ + @Override + public String toRequestId(Serializable id, Class entityType) { + return id.toString(); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return true; + } + } +} 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..e4a8c6589 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java @@ -0,0 +1,96 @@ +/* + * 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.config.ResourceMetadataHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.spi.BackendIdConverter; +import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; +import org.springframework.data.rest.webmvc.util.UriUtils; +import org.springframework.plugin.core.PluginRegistry; +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 PluginRegistry> idConverters; + private final ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver; + + /** + * Creates a new {@link BackendIdHandlerMethodArgumentResolver} for the given {@link BackendIdConverter}s and + * {@link ResourceMetadataHandlerMethodArgumentResolver}. + * + * @param idConverters the {@link BackendIdConverter}s registered in the system. + * @param resourceMetadataResolver the resolver to obtain {@link ResourceMetadata} from. + */ + public BackendIdHandlerMethodArgumentResolver(PluginRegistry> idConverters, + ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver) { + + Assert.notNull(idConverters, "Id converters must not be null!"); + Assert.notNull(resourceMetadataResolver, "ResourceMetadata resolver must not be null!"); + + this.idConverters = idConverters; + 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 Object 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); + } + + BackendIdConverter pluginFor = idConverters.getPluginFor(metadata.getDomainType(), DefaultIdConverter.INSTANCE); + return pluginFor.fromRequestId(UriUtils.findMappingVariable("id", parameter, request), metadata.getDomainType()); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java deleted file mode 100644 index 03238268c..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/HttpRequestUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.springframework.data.rest.webmvc.support; - -import javax.servlet.ServletContext; -import javax.servlet.ServletRegistration; - -import org.springframework.data.rest.webmvc.RepositoryRestDispatcherServlet; - -/** - * Helper class to HttpServletRequest helpers. - * - * @author Jon Brisbin - */ -public abstract class HttpRequestUtils { - - /** - * Strip a servlet registration mapping from the request URI. - * - * @param requestUri The request URI to strip. - * @param ctx The servlet context in which to search for registration mappings. - * @return The stripped request URI. - */ - public static String stripRegistrationMapping(String requestUri, ServletContext ctx) { - for (ServletRegistration reg : ctx.getServletRegistrations().values()) { - if (reg.getClassName().equals(RepositoryRestDispatcherServlet.class.getName()) - || reg.getName().equals("rest-exporter")) { - for (String mapping : reg.getMappings()) { - if (mapping.contains("*")) { - mapping = mapping.substring(0, mapping.indexOf('*')); - } - if (requestUri.startsWith(mapping)) { - return requestUri.replaceAll(mapping, ""); - } - } - } - } - return requestUri; - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 225d963fb..0e2429bda 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -17,12 +17,16 @@ package org.springframework.data.rest.webmvc.support; import static org.springframework.hateoas.TemplateVariable.VariableType.*; +import java.io.Serializable; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.spi.BackendIdConverter; +import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; @@ -31,6 +35,7 @@ import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.AbstractEntityLinks; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -48,6 +53,7 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { private final ResourceMappings mappings; private final RepositoryRestConfiguration config; private final HateoasPageableHandlerMethodArgumentResolver resolver; + private final PluginRegistry> idConverters; /** * Creates a new {@link RepositoryEntityLinks}. @@ -56,20 +62,24 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { * @param mappings must not be {@literal null}. * @param config must not be {@literal null}. * @param resolver must not be {@literal null}. + * @param idConverters must not be {@literal null}. */ @Autowired public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, - RepositoryRestConfiguration config, HateoasPageableHandlerMethodArgumentResolver resolver) { + RepositoryRestConfiguration config, HateoasPageableHandlerMethodArgumentResolver resolver, + PluginRegistry> idConverters) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); Assert.notNull(resolver, "HateoasPageableHandlerMethodArgumentResolver must not be null!"); + Assert.notNull(idConverters, "Id converter registry must not be null!"); this.repositories = repositories; this.mappings = mappings; this.config = config; this.resolver = resolver; + this.idConverters = idConverters; } /* @@ -134,8 +144,12 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { @Override public Link linkToSingleResource(Class type, Object id) { + Assert.isInstanceOf(Serializable.class, id, "Id must be assignable to Serializable!"); + ResourceMetadata metadata = mappings.getMappingFor(type); - Link link = linkFor(type).slash(id).withRel(metadata.getItemResourceRel()); + String mappedId = idConverters.getPluginFor(type, DefaultIdConverter.INSTANCE).toRequestId((Serializable) id, type); + + Link link = linkFor(type).slash(mappedId).withRel(metadata.getItemResourceRel()); ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); if (!projectionConfiguration.hasProjectionFor(type)) { 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/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 401c65130..07881494a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -56,6 +56,6 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll RootResourceInformation request = getResourceInformation(Address.class); - controller.postEntity(request, null, null); + controller.postCollectionResource(request, null, null); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java index ae6885c19..78adeea89 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Book.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -20,13 +20,15 @@ import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; @Entity public class Book { - @Id String isbn; + @Id @GeneratedValue Long id; + String isbn; @ManyToMany(cascade = { CascadeType.MERGE })// Set authors; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookIdConverter.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookIdConverter.java new file mode 100644 index 000000000..1945de8f3 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookIdConverter.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.jpa; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.rest.webmvc.spi.BackendIdConverter; +import org.springframework.util.StringUtils; + +/** + * {@link BackendIdConverter} artificially transforming the actual book id into some magic {@link String} and back. + * + * @author Oliver Gierke + */ +public class BookIdConverter implements BackendIdConverter { + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.BackendIdConverter#fromRequestId(java.lang.String, java.lang.Class) + */ + @Override + public Serializable fromRequestId(String id, Class entityType) { + return Long.parseLong(id.substring(0, id.indexOf('-'))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.BackendIdConverter#toRequestId(java.lang.Object, java.lang.Class) + */ + @Override + public String toRequestId(Serializable id, Class entityType) { + + Long longId = (Long) id; + List ids = new ArrayList(longId.intValue()); + + for (int i = 0; i < longId; i++) { + ids.add(longId); + } + + return StringUtils.collectionToDelimitedString(ids, "-"); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return Book.class.equals(delimiter); + } +} 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 2647cf7b8..679b9b97d 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 @@ -20,6 +20,6 @@ import org.springframework.data.repository.CrudRepository; /** * @author Oliver Gierke */ -public interface BookRepository extends CrudRepository { +public interface BookRepository extends CrudRepository { } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java index ecca733c8..565f23ecf 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -21,6 +21,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; /** + * Test configuration for JPA. + * * @author Jon Brisbin * @author Oliver Gierke */ @@ -29,6 +31,11 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement public class JpaRepositoryConfig extends JpaInfrastructureConfig { + @Bean + public BookIdConverter bookIdConverter() { + return new BookIdConverter(); + } + @Bean public TestDataPopulator testDataPopulator() { return new TestDataPopulator(); 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..c3f1f126e --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/BackendIdConverterHandlerMethodArgumentResolverIntegrationTests.java @@ -0,0 +1,81 @@ +/* + * 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", "/orders/5{?projection}")); + + Object resolvedId = resolver.resolveArgument(parameter, null, request, null); + + assertThat(resolvedId, is((Object) "5")); + } + + /** + * @see DATAREST-155 + */ + @Test + public void translatesUriToBackendId() 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-5-5-5-5")); + + Object resolvedId = resolver.resolveArgument(parameter, null, request, null); + + assertThat(resolvedId, is((Object) 5L)); + } + + static class SampleController { + + @RequestMapping("/{repository}/{id}") + void resolveId(@BackendId Serializable backendId) {} + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java index b87442518..73fa429e1 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; +import org.springframework.data.rest.webmvc.jpa.Book; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Order; import org.springframework.data.rest.webmvc.jpa.Person; @@ -69,4 +70,14 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt assertThat(link.isTemplated(), is(true)); assertThat(link.getVariableNames(), hasItem(configuration.projectionConfiguration().getParameterName())); } + + /** + * @see DATAREST-155 + */ + @Test + public void usesCustomGeneratedBackendId() { + + Link link = entityLinks.linkToSingleResource(Book.class, 7L); + assertThat(link.getHref(), endsWith("/7-7-7-7-7-7-7")); + } }