DATAREST-160 - Polished support for ETag headers based on version properties.
Introduced a dedicated value object for ETags and moved the logic of ETagValidator into it. Switched from an annotation based injection model to a type based one. The HTTP status code for a non-matching ETag is now 412 Precondition failed as the HTTP specification suggests. Original pull request: #148.
This commit is contained in:
@@ -34,6 +34,7 @@ import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.rest.core.RepositoryConstraintViolationException;
|
||||
import org.springframework.data.rest.core.mapping.ResourceMetadata;
|
||||
import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException;
|
||||
import org.springframework.data.rest.webmvc.support.ExceptionMessage;
|
||||
import org.springframework.data.rest.webmvc.support.RepositoryConstraintViolationExceptionMessage;
|
||||
import org.springframework.data.web.PagedResourcesAssembler;
|
||||
@@ -132,7 +133,6 @@ class AbstractRepositoryRestController implements MessageSourceAware {
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler({ OptimisticLockingFailureException.class, DataIntegrityViolationException.class })
|
||||
@ResponseBody
|
||||
public ResponseEntity handleConflict(Exception ex) {
|
||||
return errorResponse(null, ex, HttpStatus.CONFLICT);
|
||||
}
|
||||
@@ -144,7 +144,6 @@ class AbstractRepositoryRestController implements MessageSourceAware {
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler
|
||||
@ResponseBody
|
||||
public ResponseEntity<Void> handle(HttpRequestMethodNotSupportedException o_O) {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -153,6 +152,13 @@ class AbstractRepositoryRestController implements MessageSourceAware {
|
||||
return new ResponseEntity<Void>(headers, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<Void> handle(ETagDoesntMatchException o_O) {
|
||||
|
||||
HttpHeaders headers = o_O.getExpectedETag().addTo(new HttpHeaders());
|
||||
return new ResponseEntity<Void>(headers, HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
protected <T> ResponseEntity<T> notFound() {
|
||||
return notFound(null, null);
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ import org.springframework.data.rest.core.mapping.SearchResourceMappings;
|
||||
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
|
||||
import org.springframework.data.rest.webmvc.support.BackendId;
|
||||
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
|
||||
import org.springframework.data.rest.webmvc.support.IfMatch;
|
||||
import org.springframework.data.rest.webmvc.support.ETag;
|
||||
import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException;
|
||||
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
|
||||
import org.springframework.data.rest.webmvc.support.EtagValidator;
|
||||
import org.springframework.data.web.PagedResourcesAssembler;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.PagedResources;
|
||||
@@ -62,7 +62,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@@ -84,21 +83,19 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
private final RepositoryEntityLinks entityLinks;
|
||||
private final RepositoryRestConfiguration config;
|
||||
private final ConversionService conversionService;
|
||||
private final EtagValidator etagValidator;
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
@Autowired
|
||||
public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config,
|
||||
RepositoryEntityLinks entityLinks, PagedResourcesAssembler<Object> assembler,
|
||||
@Qualifier("defaultConversionService") ConversionService conversionService, EtagValidator etagValidator) {
|
||||
@Qualifier("defaultConversionService") ConversionService conversionService) {
|
||||
|
||||
super(assembler);
|
||||
|
||||
this.entityLinks = entityLinks;
|
||||
this.config = config;
|
||||
this.conversionService = conversionService;
|
||||
this.etagValidator = etagValidator;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -300,11 +297,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
return new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
PersistentEntityResource persistentEntityResource = assembler.toFullResource(domainObj);
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
etagValidator.addEtagHeader(httpHeaders, persistentEntityResource);
|
||||
PersistentEntityResource resource = assembler.toFullResource(domainObj);
|
||||
HttpHeaders headers = ETag.from(resource).addTo(new HttpHeaders());
|
||||
|
||||
return new ResponseEntity<Resource<?>>(persistentEntityResource, httpHeaders, HttpStatus.OK);
|
||||
return new ResponseEntity<Resource<?>>(resource, headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,8 +314,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
* @throws HttpRequestMethodNotSupportedException
|
||||
*/
|
||||
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT)
|
||||
public ResponseEntity<? extends ResourceSupport> putItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id,
|
||||
PersistentEntityResourceAssembler assembler, @IfMatch String eTagMatch) throws HttpRequestMethodNotSupportedException {
|
||||
public ResponseEntity<? extends ResourceSupport> putItemResource(RootResourceInformation resourceInformation,
|
||||
PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler,
|
||||
ETag eTag) throws HttpRequestMethodNotSupportedException {
|
||||
|
||||
resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM);
|
||||
|
||||
@@ -332,9 +329,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
Object objectToSave = incomingWrapper.getBean();
|
||||
|
||||
Object domainObject = invoker.invokeFindOne(id);
|
||||
if (domainObject != null) {
|
||||
etagValidator.validateEtag(eTagMatch, resourceInformation, domainObject);
|
||||
}
|
||||
|
||||
eTag.verify(resourceInformation.getPersistentEntity(), domainObject);
|
||||
|
||||
return domainObject == null ? createAndReturn(objectToSave, invoker, assembler) : saveAndReturn(objectToSave,
|
||||
invoker, PUT, assembler);
|
||||
@@ -343,18 +339,20 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
/**
|
||||
* <code>PUT /{repository}/{id}</code> - Updates an existing entity or creates one at exactly that place.
|
||||
*
|
||||
* @param eTagMatch
|
||||
* @param resourceInformation
|
||||
* @param payload
|
||||
* @param id
|
||||
* @param assembler
|
||||
* @param eTag
|
||||
* @return
|
||||
* @throws HttpRequestMethodNotSupportedException
|
||||
* @throws ResourceNotFoundException
|
||||
* @throws ETagDoesntMatchException
|
||||
*/
|
||||
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH)
|
||||
public ResponseEntity<ResourceSupport> patchItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id,
|
||||
PersistentEntityResourceAssembler assembler, @IfMatch String eTagMatch) throws HttpRequestMethodNotSupportedException,
|
||||
ResourceNotFoundException {
|
||||
public ResponseEntity<ResourceSupport> patchItemResource(RootResourceInformation resourceInformation,
|
||||
PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler,
|
||||
ETag eTag) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException {
|
||||
|
||||
resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM);
|
||||
|
||||
@@ -364,7 +362,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
etagValidator.validateEtag(eTagMatch, resourceInformation, domainObject);
|
||||
eTag.verify(resourceInformation.getPersistentEntity(), domainObject);
|
||||
|
||||
return saveAndReturn(payload.getContent(), resourceInformation.getInvoker(), PATCH, assembler);
|
||||
}
|
||||
@@ -372,16 +370,17 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
/**
|
||||
* <code>DELETE /{repository}/{id}</code> - Deletes the entity backing the item resource.
|
||||
*
|
||||
* @param eTagMatch
|
||||
* @param resourceInformation
|
||||
* @param id
|
||||
* @param eTag
|
||||
* @return
|
||||
* @throws ResourceNotFoundException
|
||||
* @throws HttpRequestMethodNotSupportedException
|
||||
* @throws ETagDoesntMatchException
|
||||
*/
|
||||
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<?> deleteItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id, @IfMatch String eTagMatch)
|
||||
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
|
||||
public ResponseEntity<?> deleteItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id,
|
||||
ETag eTag) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
|
||||
|
||||
resourceInformation.verifySupportedMethod(HttpMethod.DELETE, ResourceType.ITEM);
|
||||
|
||||
@@ -392,7 +391,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
etagValidator.validateEtag(eTagMatch, resourceInformation, domainObj);
|
||||
eTag.verify(resourceInformation.getPersistentEntity(), domainObj);
|
||||
|
||||
publisher.publishEvent(new BeforeDeleteEvent(domainObj));
|
||||
invoker.invokeDelete(id);
|
||||
@@ -424,10 +423,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
|
||||
if (config.isReturnBodyOnUpdate()) {
|
||||
|
||||
PersistentEntityResource persistentEntityResource = assembler.toFullResource(obj);
|
||||
etagValidator.addEtagHeader(headers, persistentEntityResource);
|
||||
PersistentEntityResource resource = assembler.toFullResource(obj);
|
||||
headers = ETag.from(resource).addTo(headers);
|
||||
|
||||
return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, persistentEntityResource);
|
||||
return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, resource);
|
||||
} else {
|
||||
return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers);
|
||||
}
|
||||
@@ -451,10 +450,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
addLocationHeader(headers, assembler, savedObject);
|
||||
|
||||
PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toFullResource(savedObject) : null;
|
||||
|
||||
if (resource != null) {
|
||||
etagValidator.addEtagHeader(headers, resource);
|
||||
}
|
||||
headers = ETag.from(resource).addTo(headers);
|
||||
|
||||
return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource);
|
||||
}
|
||||
|
||||
@@ -88,11 +88,10 @@ 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.DefaultedPageableHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.support.ETagArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.support.IfMatchHeaderArgumentResolver;
|
||||
import org.springframework.data.rest.webmvc.support.JpaHelper;
|
||||
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
|
||||
import org.springframework.data.rest.webmvc.support.EtagValidator;
|
||||
import org.springframework.data.util.AnnotatedTypeScanner;
|
||||
import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver;
|
||||
import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver;
|
||||
@@ -309,9 +308,10 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IfMatchHeaderArgumentResolver ifMatchHeaderArgumentResolver(){
|
||||
return new IfMatchHeaderArgumentResolver();
|
||||
public ETagArgumentResolver eTagArgumentResolver() {
|
||||
return new ETagArgumentResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current
|
||||
* configuration into account when generating links.
|
||||
@@ -621,11 +621,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
return OrderAwarePluginRegistry.create(converters);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EtagValidator etagValidator() {
|
||||
return new EtagValidator(defaultConversionService());
|
||||
}
|
||||
|
||||
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
|
||||
|
||||
PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver(
|
||||
@@ -639,7 +634,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
return Arrays.asList(defaultedPageableResolver, pageableResolver, sortResolver(),
|
||||
serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
|
||||
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
|
||||
peraResolver, backendIdHandlerMethodArgumentResolver(), ifMatchHeaderArgumentResolver());
|
||||
peraResolver, backendIdHandlerMethodArgumentResolver(), eTagArgumentResolver());
|
||||
}
|
||||
|
||||
@Autowired GeoModule geoModule;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.springframework.util.StringUtils.*;
|
||||
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A value object to represent ETags.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public final class ETag {
|
||||
|
||||
public static final ETag NO_ETAG = new ETag(null);
|
||||
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ETag} from the given value.
|
||||
*
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
private ETag(String value) {
|
||||
this.value = trimTrailingCharacter(trimLeadingCharacter(value, '"'), '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ETag} for the given {@link String} value. Falls back to {@link #NO_ETAG} in case
|
||||
* {@literal null} is provided.
|
||||
*
|
||||
* @param value the source ETag value, can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static ETag from(String value) {
|
||||
return value == null ? NO_ETAG : new ETag(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ETag} for the given {@link PersistentEntityResource}.
|
||||
*
|
||||
* @param resource can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static ETag from(PersistentEntityResource resource) {
|
||||
return resource == null ? NO_ETAG : from(resource.getPersistentEntity(), resource.getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the ETag to be created for the given target bean with the current one and raises a
|
||||
* {@link ETagDoesntMatchException} in case they don't match.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @param target can be {@literal null}.
|
||||
* @throws ETagDoesntMatchException in case the calculated {@link ETag} for the given bean does not match the current
|
||||
* one.
|
||||
*/
|
||||
public void verify(PersistentEntity<?, ?> entity, Object target) {
|
||||
|
||||
if (this == NO_ETAG || target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.equals(from(entity, target))) {
|
||||
throw new ETagDoesntMatchException(target, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current {@link ETag} to the given headers.
|
||||
*
|
||||
* @param headers must not be {@literal null}.
|
||||
* @return the {@link HttpHeaders} with the ETag header been set if the current {@link ETag} instance is not
|
||||
* {@link #NO_ETAG}.
|
||||
*/
|
||||
public HttpHeaders addTo(HttpHeaders headers) {
|
||||
|
||||
Assert.notNull(headers, "HttpHeaders must not be null!");
|
||||
String stringValue = toString();
|
||||
|
||||
if (stringValue == null) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
headers.setETag(stringValue);
|
||||
return headers;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return value == null ? null : "\"".concat(value).concat("\"");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof ETag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ETag that = (ETag) obj;
|
||||
|
||||
return ObjectUtils.nullSafeEquals(this.value, that.value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ETag} from the given {@link PersistentEntity} and target bean.
|
||||
*
|
||||
* @param entity
|
||||
* @param bean
|
||||
* @return
|
||||
*/
|
||||
private static ETag from(PersistentEntity<?, ?> entity, Object bean) {
|
||||
return from(getVersionInformation(entity, bean));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quoted version property of a domain object, returns null if it doesn't contains the property
|
||||
*
|
||||
* @param entity
|
||||
* @param bean
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static String getVersionInformation(PersistentEntity entity, Object bean) {
|
||||
|
||||
Assert.notNull(entity, "PersistentEntity must not be null!");
|
||||
Assert.notNull(bean, "Target bean must not be null!");
|
||||
|
||||
if (!entity.hasVersionProperty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(bean);
|
||||
return accessor.getProperty(entity.getVersionProperty()).toString();
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
@@ -28,8 +26,9 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
* {@link HandlerMethodArgumentResolver} to resolve If-Match headers for optimistic locking handling {@link IfMatch}.
|
||||
*
|
||||
* @author Pablo Lozano
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class IfMatchHeaderArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
public class ETagArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@@ -37,7 +36,7 @@ public class IfMatchHeaderArgumentResolver implements HandlerMethodArgumentResol
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(IfMatch.class);
|
||||
return parameter.getParameterType().equals(ETag.class);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -45,16 +44,8 @@ public class IfMatchHeaderArgumentResolver implements HandlerMethodArgumentResol
|
||||
* @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,
|
||||
public ETag resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
IfMatch ifMatchHeader = parameter.getParameterAnnotation(IfMatch.class);
|
||||
HttpServletRequest httprequest = (HttpServletRequest) webRequest.getNativeRequest();
|
||||
Object result = httprequest.getHeader("If-Match");
|
||||
|
||||
if (result == null) {
|
||||
result = ifMatchHeader.defaultValue();
|
||||
}
|
||||
return result;
|
||||
return ETag.from(webRequest.getHeader("If-Match"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.support;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An exception being thrown in case the {@link ETag} calculated for a particular object does not match an expected one.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @see ETag#verify(org.springframework.data.mapping.PersistentEntity, Object)
|
||||
*/
|
||||
public class ETagDoesntMatchException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 415835592506644699L;
|
||||
|
||||
private final ETag expected;
|
||||
private final Object bean;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ETagDoesntMatchException} for the given bean as well as the {@link ETag} it was expected to
|
||||
* match.
|
||||
*
|
||||
* @param bean must not be {@literal null}.
|
||||
* @param expected must not be {@literal null}.
|
||||
*/
|
||||
public ETagDoesntMatchException(Object bean, ETag expected) {
|
||||
|
||||
Assert.notNull(bean, "Target bean must not be null!");
|
||||
Assert.notNull(expected, "Expected ETag must not be null!");
|
||||
|
||||
this.expected = expected;
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bean not matching the {@link ETag}.
|
||||
*
|
||||
* @return the bean
|
||||
*/
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ETag} the bean was expected to match.
|
||||
*
|
||||
* @return the expected
|
||||
*/
|
||||
public ETag getExpectedETag() {
|
||||
return expected;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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 org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.model.BeanWrapper;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResource;
|
||||
import org.springframework.data.rest.webmvc.RootResourceInformation;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
|
||||
/**
|
||||
* An ETag validator that verifies concurrency issues and the validity of the Etag/ If-Match header values to implement
|
||||
* optimistic locking.
|
||||
*
|
||||
* @author Pablo Lozano
|
||||
*/
|
||||
|
||||
public class EtagValidator {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public EtagValidator(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
Assert.notNull(conversionService, "Conversion service must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the given eTag with the entity's version property, if there are different an
|
||||
* OptimisticLockingFailureException is thrown.
|
||||
*
|
||||
* @param requestEtag
|
||||
* @param resourceInformation
|
||||
* @param domainObject
|
||||
* @return
|
||||
*/
|
||||
public void validateEtag(String requestEtag, RootResourceInformation resourceInformation, Object domainObject) {
|
||||
|
||||
if (requestEtag != null && !requestEtag.equals(ValueConstants.DEFAULT_NONE)) {
|
||||
final String entityEtag = getVersionInformation(resourceInformation.getPersistentEntity(), domainObject);
|
||||
if (!requestEtag.equals(entityEtag)) {
|
||||
throw new OptimisticLockingFailureException("Invalid If-Match version provided, the resource has gone thru"
|
||||
+ " changes after resource's request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Etag to the header, if the domain object does not contain a Version property it will return and leave the
|
||||
* headers as is.
|
||||
*
|
||||
* @param headers
|
||||
* @param persistentEntityResource
|
||||
*/
|
||||
public void addEtagHeader(HttpHeaders headers, PersistentEntityResource persistentEntityResource) {
|
||||
|
||||
String version = getVersionInformation(persistentEntityResource.getPersistentEntity(),
|
||||
persistentEntityResource.getContent());
|
||||
if (version != null) {
|
||||
headers.setETag(version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quoted version property of a domain object, returns null if it doesn't contains the property
|
||||
*
|
||||
* @param persistentEntity
|
||||
* @param domainObject
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private String getVersionInformation(PersistentEntity persistentEntity, Object domainObject) {
|
||||
|
||||
if (persistentEntity.hasVersionProperty()) {
|
||||
|
||||
BeanWrapper<Object> beanWrapper = BeanWrapper.create(domainObject, conversionService);
|
||||
Object version = beanWrapper.getProperty(persistentEntity.getVersionProperty());
|
||||
return "\"" + version.toString() + "\"";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.springframework.data.rest.webmvc.jpa.CreditCard;
|
||||
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
|
||||
import org.springframework.data.rest.webmvc.jpa.Order;
|
||||
import org.springframework.data.rest.webmvc.jpa.Person;
|
||||
import org.springframework.data.rest.webmvc.support.ETag;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -89,7 +90,8 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
|
||||
PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()),
|
||||
entities.getPersistentEntity(Order.class)).build();
|
||||
|
||||
ResponseEntity<?> entity = controller.putItemResource(information, persistentEntityResource, 1L, assembler, null);
|
||||
ResponseEntity<?> entity = controller.putItemResource(information, persistentEntityResource, 1L, assembler,
|
||||
ETag.NO_ETAG);
|
||||
|
||||
assertThat(entity.getHeaders().getLocation().toString(), not(endsWith("{?projection}")));
|
||||
}
|
||||
|
||||
@@ -611,7 +611,7 @@ public class JpaWebTests extends CommonWebTests {
|
||||
mvc.perform(
|
||||
patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyTequila\" }")
|
||||
.contentType(MediaType.APPLICATION_JSON).header("If-Match", "\"falseETag\"")).andExpect(
|
||||
status().isConflict());
|
||||
status().isPreconditionFailed());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,12 @@ package org.springframework.data.rest.webmvc.mongodb;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -34,10 +33,11 @@ import org.springframework.hateoas.Link;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
|
||||
/**
|
||||
* Integration tests for MongoDB repositories.
|
||||
*
|
||||
@@ -50,7 +50,7 @@ public class MongoWebTests extends CommonWebTests {
|
||||
@Autowired ProfileRepository repository;
|
||||
@Autowired UserRepository userRepository;
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Before
|
||||
public void populateProfiles() {
|
||||
@@ -66,7 +66,7 @@ public class MongoWebTests extends CommonWebTests {
|
||||
repository.save(Arrays.asList(twitter, linkedIn));
|
||||
|
||||
Address address = new Address();
|
||||
address.street = "Foo";
|
||||
address.street = "ETagDoesntMatchExceptionUnitTests";
|
||||
address.zipCode = "Bar";
|
||||
|
||||
User user = new User();
|
||||
@@ -160,7 +160,8 @@ public class MongoWebTests extends CommonWebTests {
|
||||
*/
|
||||
@Test
|
||||
public void returnConflictWhenConcurrentlyEditingVersionedEntity() throws Exception {
|
||||
Link receiptLink = discoverUnique("receipts");
|
||||
|
||||
Link receiptLink = client.discoverUnique("receipts");
|
||||
|
||||
Receipt receipt = new Receipt();
|
||||
receipt.setAmount(new BigDecimal(50));
|
||||
@@ -169,7 +170,7 @@ public class MongoWebTests extends CommonWebTests {
|
||||
String stringReceipt = mapper.writeValueAsString(receipt);
|
||||
|
||||
MockHttpServletResponse createdReceipt = postAndGet(receiptLink, stringReceipt, MediaType.APPLICATION_JSON);
|
||||
Link tacosLink = assertHasLinkWithRel("self", createdReceipt);
|
||||
Link tacosLink = client.assertHasLinkWithRel("self", createdReceipt);
|
||||
assertJsonPathEquals("$.saleItem", "Springy Tacos", createdReceipt);
|
||||
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(tacosLink.getHref());
|
||||
@@ -183,6 +184,6 @@ public class MongoWebTests extends CommonWebTests {
|
||||
mvc.perform(
|
||||
patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyTequila\" }")
|
||||
.contentType(MediaType.APPLICATION_JSON).header("If-Match", concurrencyTag)).andExpect(
|
||||
status().isConflict());
|
||||
status().isPreconditionFailed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,30 @@
|
||||
* 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;
|
||||
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Annotation to find the If-Match header of a request
|
||||
*
|
||||
* @author Pablo Lozano
|
||||
* Unit tests for {@link ETagDoesntMatchException}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class ETagDoesntMatchExceptionUnitTests {
|
||||
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface IfMatch {
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullTargetBean() {
|
||||
new ETagDoesntMatchException(null, ETag.from("1"));
|
||||
}
|
||||
|
||||
String defaultValue() default ValueConstants.DEFAULT_NONE;
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void rejectsNullExpectedETag() {
|
||||
new ETagDoesntMatchException(new Object(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
/**
|
||||
* Tests for the ETagValidator used for optimistic locking
|
||||
*
|
||||
* @author Pablo Lozano
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ETagUnitTests {
|
||||
|
||||
MongoMappingContext context = new MongoMappingContext();
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test(expected = ETagDoesntMatchException.class)
|
||||
public void expectWrongEtag() throws Exception {
|
||||
|
||||
ETag eTag = ETag.from("1");
|
||||
eTag.verify(context.getPersistentEntity(Sample.class), new Sample(0L));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void expectCorrectEtag() throws Exception {
|
||||
ETag.from("0").verify(context.getPersistentEntity(Sample.class), new Sample(0L));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void createsETagFromVersionValue() throws Exception {
|
||||
|
||||
PersistentEntity<?, ?> entity = context.getPersistentEntity(Sample.class);
|
||||
ETag from = ETag.from(PersistentEntityResource.build(new Sample(0L), entity).build());
|
||||
|
||||
assertThat(from.toString(), is((Object) "\"0\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void surroundsValueWithQuotationMarksOnToString() {
|
||||
assertThat(ETag.from("1").toString(), is("\"1\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void returnsNoEtagForNullStringSource() {
|
||||
assertThat(ETag.from((String) null), is(ETag.NO_ETAG));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void returnsNoEtagForNullPersistentEntityResourceSource() {
|
||||
assertThat(ETag.from((PersistentEntityResource) null), is(ETag.NO_ETAG));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void hasValueObjectEqualsSemantics() {
|
||||
|
||||
ETag one = ETag.from("1");
|
||||
ETag two = ETag.from("2");
|
||||
ETag nullETag = ETag.from((String) null);
|
||||
|
||||
assertThat(one.equals(one), is(true));
|
||||
assertThat(one.equals(two), is(false));
|
||||
assertThat(two.equals(one), is(false));
|
||||
assertThat(nullETag.equals(one), is(false));
|
||||
assertThat(one.equals(two), is(false));
|
||||
assertThat(one.equals(""), is(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void returnsNoEtagForEntityWithoutVersionProperty() {
|
||||
|
||||
PersistentEntity<?, ?> entity = context.getPersistentEntity(SampleWithoutVersion.class);
|
||||
assertThat(ETag.from(PersistentEntityResource.build(new SampleWithoutVersion(), entity).build()), is(ETag.NO_ETAG));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void noETagReturnsNullForToString() {
|
||||
assertThat(ETag.NO_ETAG.toString(), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void noETagDoesNotRejectVerification() {
|
||||
ETag.NO_ETAG.verify(context.getPersistentEntity(Sample.class), new Sample(5L));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void verificationDoesNotRejectNullEntity() {
|
||||
ETag.from("5").verify(context.getPersistentEntity(Sample.class), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void stripsTrailingAndLeadingQuotesOnCreation() {
|
||||
|
||||
assertThat(ETag.from("\"1\""), is(ETag.from("1")));
|
||||
assertThat(ETag.from("\"\"1\"\""), is(ETag.from("1")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void addsETagToHeadersIfNotNoETag() {
|
||||
|
||||
HttpHeaders headers = ETag.from("1").addTo(new HttpHeaders());
|
||||
assertThat(headers.getETag(), is(notNullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void doesNotAddHeaderForNoETag() {
|
||||
|
||||
HttpHeaders headers = ETag.NO_ETAG.addTo(new HttpHeaders());
|
||||
|
||||
assertThat(headers.containsKey("ETag"), is(false));
|
||||
}
|
||||
|
||||
public class Sample {
|
||||
|
||||
@Version Long version;
|
||||
|
||||
Sample(Long version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
public class SampleWithoutVersion {}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* 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.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import javax.persistence.Version;
|
||||
import javax.persistence.metamodel.Metamodel;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
|
||||
import org.springframework.data.jpa.mapping.JpaPersistentEntity;
|
||||
import org.springframework.data.rest.core.mapping.MappingResourceMetadata;
|
||||
import org.springframework.data.rest.core.mapping.ResourceMetadata;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResource;
|
||||
import org.springframework.data.rest.webmvc.RootResourceInformation;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
/**
|
||||
* Tests for the EtagValidator used for optimistic locking
|
||||
*
|
||||
* @author Pablo Lozano
|
||||
*/
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class EtagValidatorTests {
|
||||
|
||||
EtagValidator etagValidator;
|
||||
|
||||
@Mock Metamodel model;
|
||||
|
||||
JpaMetamodelMappingContext context;
|
||||
JpaPersistentEntity<?> entity;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = new JpaMetamodelMappingContext(model);
|
||||
entity = context.getPersistentEntity(Sample.class);
|
||||
etagValidator = new EtagValidator(new DefaultFormattingConversionService());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test(expected = OptimisticLockingFailureException.class)
|
||||
public void expectWrongEtag() throws Exception {
|
||||
Sample sampleEntity = new Sample();
|
||||
sampleEntity.version = 0;
|
||||
|
||||
ResourceMetadata resourceMetadata = new MappingResourceMetadata(entity);
|
||||
RootResourceInformation rootResourceInformation = new RootResourceInformation(resourceMetadata, entity, null);
|
||||
|
||||
etagValidator.validateEtag("\"1\"", rootResourceInformation, sampleEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void expectCorrectEtag() throws Exception {
|
||||
Sample sampleEntity = new Sample();
|
||||
sampleEntity.version = 0;
|
||||
|
||||
ResourceMetadata resourceMetadata = new MappingResourceMetadata(entity);
|
||||
RootResourceInformation rootResourceInformation = new RootResourceInformation(resourceMetadata, entity, null);
|
||||
|
||||
etagValidator.validateEtag("\"0\"", rootResourceInformation, sampleEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREST-160
|
||||
*/
|
||||
@Test
|
||||
public void setCorrectEtagHeader() throws Exception {
|
||||
Sample sampleEntity = new Sample();
|
||||
sampleEntity.version = 0;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
PersistentEntityResource perf = PersistentEntityResource.build(sampleEntity, entity).build();
|
||||
etagValidator.addEtagHeader(headers, perf);
|
||||
Object ifMatch = headers.getETag();
|
||||
|
||||
assertThat(ifMatch, is((Object) "\"0\""));
|
||||
}
|
||||
|
||||
public class Sample {
|
||||
|
||||
private @Version long version;
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user