DATAREST-155 - Introduce API to alter exposed backend ids.

Introduced BackendIdConverter SPI to be able to register custom components that will be used during URI construction and URI parsing to determine the actual backend identifier.

To register a custom BackendIdConverter simply declare a bean definition for it in the ApplicationContext. The Spring Data REST URI creation infrastructure (RepositoryEntityLinks in particular) will pick it up transparently.
This commit is contained in:
Oliver Gierke
2014-03-11 17:41:17 +01:00
parent 0a3738782d
commit 572690b888
19 changed files with 529 additions and 163 deletions

View File

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

View File

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

View File

@@ -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<ResourceSupport> postEntity(RootResourceInformation resourceInformation,
public ResponseEntity<ResourceSupport> 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<Resource<?>> 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<? extends ResourceSupport> putEntity(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> payload, @PathVariable String id, PersistentEntityResourceAssembler assembler)
public ResponseEntity<? extends ResourceSupport> putItemResource(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> 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<ResourceSupport> patchEntity(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> payload, @PathVariable String id, PersistentEntityResourceAssembler assembler)
public ResponseEntity<ResourceSupport> patchItemResource(RootResourceInformation resourceInformation,
PersistentEntityResource<Object> 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);

View File

@@ -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<ResourceSupport> 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<? extends ResourceSupport> 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<ResourceSupport> 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<ResourceSupport> followPropertyReferenceCompact(RootResourceInformation repoRequest,
@PathVariable String id, @PathVariable String property, PersistentEntityResourceAssembler assembler)
@BackendId Serializable id, @PathVariable String property, PersistentEntityResourceAssembler assembler)
throws Exception {
ResponseEntity<ResourceSupport> response = followPropertyReference(repoRequest, id, property, assembler);
@@ -294,7 +296,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@ResponseBody
public ResponseEntity<? extends ResourceSupport> createPropertyReference(
final RootResourceInformation resourceInformation, final HttpMethod requestMethod,
final @RequestBody Resources<Object> incoming, @PathVariable String id, @PathVariable String property)
final @RequestBody Resources<Object> 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<ResourceSupport> 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<ReferencedProperty, ResourceSupport> handler, HttpMethod method) throws Exception {
private ResourceSupport doWithReferencedProperty(RootResourceInformation repoRequest, Serializable id,
String propertyPath, Function<ReferencedProperty, ResourceSupport> handler, HttpMethod method) throws Exception {
RepositoryInvoker invoker = repoRequest.getInvoker();

View File

@@ -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<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
@Autowired(required = false) List<MappingContext<?, ?>> mappingContexts = Collections.emptyList();
@Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
@Autowired(required = false) List<BackendIdConverter> 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<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(idConverters.size());
converters.addAll(this.idConverters);
converters.add(DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
private List<HandlerMethodArgumentResolver> 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() {

View File

@@ -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;
}
}

View File

@@ -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<Class<?>> {
/**
* 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;
}
}
}

View File

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

View File

@@ -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<BackendIdConverter, Class<?>> 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<BackendIdConverter, Class<?>> 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());
}
}

View File

@@ -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;
}
}

View File

@@ -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<BackendIdConverter, Class<?>> 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<BackendIdConverter, Class<?>> 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)) {

View File

@@ -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<String, String> 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();
}
}

View File

@@ -56,6 +56,6 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
RootResourceInformation request = getResourceInformation(Address.class);
controller.postEntity(request, null, null);
controller.postCollectionResource(request, null, null);
}
}

View File

@@ -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<Author> authors;

View File

@@ -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<Long> ids = new ArrayList<Long>(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);
}
}

View File

@@ -20,6 +20,6 @@ import org.springframework.data.repository.CrudRepository;
/**
* @author Oliver Gierke
*/
public interface BookRepository extends CrudRepository<Book, String> {
public interface BookRepository extends CrudRepository<Book, Long> {
}

View File

@@ -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();

View File

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

View File

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