DATAREST-835 - Search resources returning a single resource now get and consider ETag and Last-Modified headers.

We now interpret If-None-Match and If-Modified-Since headers on requests to resources backed by query methods returning a single instance only. This allows clients to optimize GET requests to those resources to save bandwidth.
This commit is contained in:
Oliver Gierke
2016-06-10 23:22:07 +02:00
parent 42378ab34d
commit aa7eec4683
11 changed files with 361 additions and 72 deletions

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016 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;
/**
* Mimics Java 8's Supplier interface to allow deferring a computation.
*
* @author Oliver Gierke
* @since 2.6
* @soundtrack KRS-One - Sound Of Da Police (Return Of The Boom Bap)
*/
public interface Supplier<T> {
T get();
}

View File

@@ -42,12 +42,12 @@ import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.ETag;
import org.springframework.hateoas.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.HttpRequestMethodNotSupportedException;
/**
@@ -273,7 +273,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
Mockito.when(assembler.toFullResource(Mockito.any(Object.class))).thenReturn(resource);
ResponseEntity<Resource<?>> entity = controller.getItemResource(getResourceInformation(Address.class), address.id,
assembler, new LinkedMultiValueMap<String, String>());
assembler, new HttpHeaders());
assertThat(entity.getHeaders().getETag(), is(notNullValue()));
}

View File

@@ -39,6 +39,7 @@ import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
@@ -100,8 +101,8 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>(1);
parameters.add("firstname", "John");
ResponseEntity<Object> response = controller.executeSearch(resourceInformation, parameters, "firstname", PAGEABLE,
null, assembler);
ResponseEntity<?> response = controller.executeSearch(resourceInformation, parameters, "firstname", PAGEABLE, null,
assembler, new HttpHeaders());
ResourceTester tester = ResourceTester.of(response.getBody());
PagedResources<Object> pagedResources = tester.assertIsPage();
@@ -190,8 +191,8 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
RootResourceInformation resourceInformation = getResourceInformation(Book.class);
ResponseEntity<Object> result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains",
PAGEABLE, null, assembler);
ResponseEntity<?> result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains",
PAGEABLE, null, assembler, new HttpHeaders());
assertThat(result.getBody(), is(instanceOf(Resources.class)));
}

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.mongodb.core.mapping.Document;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -36,6 +37,7 @@ public class Profile {
private Long person;
private @JsonProperty(required = true) String type;
private @LastModifiedDate Date lastModifiedDate;
private @Version Long version;
private @JsonProperty("renamed") String aliased;
private Map<String, String> metadata = new HashMap<String, String>();

View File

@@ -343,4 +343,34 @@ public class MongoWebTests extends CommonWebTests {
mvc.perform(get(href)).andExpect(status().isOk());
}
/**
* @see DATAREST-835
*/
@Test
public void exposesETagHeaderForSearchResourceYieldingItemResource() throws Exception {
Link link = client.discoverUnique("profiles", "search", "findById");
Profile profile = repository.findAll().iterator().next();
mvc.perform(get(link.expand(profile.getId()).getHref()))//
.andExpect(header().string("ETag", is("\"0\"")))//
.andExpect(header().string("Last-Modified", is(notNullValue())));
}
/**
* @see DATAREST-835
*/
@Test
public void doesNotAddETagHeaderForCollectionQueryResource() throws Exception {
Link link = client.discoverUnique("profiles", "search", "findByType");
Profile profile = repository.findAll().iterator().next();
mvc.perform(get(link.expand(profile.getType()).getHref()))//
.andExpect(header().string("ETag", is(nullValue())))//
.andExpect(header().string("Last-Modified", is(nullValue())));
}
}

View File

@@ -27,11 +27,9 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrappers;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**
@@ -83,30 +81,6 @@ class AbstractRepositoryRestController {
}
}
/**
* Turns the given source into a {@link ResourceSupport} if needed and possible. Uses the given
* {@link PersistentEntityResourceAssembler} for the actual conversion.
*
* @param source can be must not be {@literal null}.
* @param assembler must not be {@literal null}.
* @param domainType the domain type in case the source is an empty iterable, must not be {@literal null}.
* @param baseLink can be {@literal null}.
* @return
*/
protected Object toResource(Object source, PersistentEntityResourceAssembler assembler, Class<?> domainType,
Link baseLink) {
if (source instanceof Iterable) {
return toResources((Iterable<?>) source, assembler, domainType, baseLink);
} else if (source == null) {
throw new ResourceNotFoundException();
} else if (ClassUtils.isPrimitiveOrWrapper(source.getClass())) {
return source;
}
return assembler.toFullResource(source);
}
protected Resources<?> entitiesToResources(Page<Object> page, PersistentEntityResourceAssembler assembler,
Class<?> domainType, Link baseLink) {

View File

@@ -44,6 +44,7 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
import org.springframework.data.rest.core.util.Supplier;
import org.springframework.data.rest.webmvc.support.BackendId;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.ETag;
@@ -62,7 +63,6 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -90,6 +90,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
private final RepositoryEntityLinks entityLinks;
private final RepositoryRestConfiguration config;
private final HttpHeadersPreparer headersPreparer;
private final ResourceStatus resourceStatus;
private ApplicationEventPublisher publisher;
@@ -107,13 +108,14 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@Autowired
public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config,
RepositoryEntityLinks entityLinks, PagedResourcesAssembler<Object> assembler,
AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
HttpHeadersPreparer headersPreparer) {
super(assembler);
this.entityLinks = entityLinks;
this.config = config;
this.headersPreparer = new HttpHeadersPreparer(auditableBeanWrapperFactory);
this.headersPreparer = headersPreparer;
this.resourceStatus = ResourceStatus.of(headersPreparer);
}
/*
@@ -187,7 +189,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET)
public Resources<?> getCollectionResource(@QuerydslPredicate RootResourceInformation resourceInformation,
DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION);
@@ -232,7 +234,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
produces = { "application/x-spring-data-compact+json", "text/uri-list" })
public Resources<?> getCollectionResourceCompact(@QuerydslPredicate RootResourceInformation resourceinformation,
DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
Resources<?> resources = getCollectionResource(resourceinformation, pageable, sort, assembler);
List<Link> links = new ArrayList<Link>(resources.getLinks());
@@ -263,7 +265,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
public ResponseEntity<ResourceSupport> postCollectionResource(RootResourceInformation resourceInformation,
PersistentEntityResource payload, PersistentEntityResourceAssembler assembler,
@RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader)
throws HttpRequestMethodNotSupportedException {
throws HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION);
@@ -327,37 +329,24 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET)
public ResponseEntity<Resource<?>> getItemResource(RootResourceInformation resourceInformation,
@BackendId Serializable id, PersistentEntityResourceAssembler assembler,
@RequestHeader MultiValueMap<String, String> rawHeaders) throws HttpRequestMethodNotSupportedException {
@BackendId Serializable id, final PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers)
throws HttpRequestMethodNotSupportedException {
Object domainObj = getItemResource(resourceInformation, id);
final Object domainObj = getItemResource(resourceInformation, id);
if (domainObj == null) {
return new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND);
}
HttpHeaders headers = new HttpHeaders();
headers.putAll(rawHeaders);
// Check ETag for If-Non-Match
List<String> ifNoneMatch = headers.getIfNoneMatch();
ETag eTag = ifNoneMatch.isEmpty() ? ETag.NO_ETAG : ETag.from(ifNoneMatch.get(0));
PersistentEntity<?, ?> entity = resourceInformation.getPersistentEntity();
HttpHeaders responseHeaders = headersPreparer.prepareHeaders(entity, domainObj);
if (eTag.matches(entity, domainObj)) {
return new ResponseEntity<Resource<?>>(responseHeaders, HttpStatus.NOT_MODIFIED);
}
// Check last modification for If-Modified-Since
if (headersPreparer.isObjectStillValid(domainObj, headers)) {
return new ResponseEntity<Resource<?>>(responseHeaders, HttpStatus.NOT_MODIFIED);
}
PersistentEntityResource resource = assembler.toFullResource(domainObj);
return new ResponseEntity<Resource<?>>(resource, responseHeaders, HttpStatus.OK);
return resourceStatus.getStatusAndHeaders(headers, domainObj, entity).toResponseEntity(//
new Supplier<PersistentEntityResource>() {
@Override
public PersistentEntityResource get() {
return assembler.toFullResource(domainObj);
}
});
}
/**
@@ -376,7 +365,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
public ResponseEntity<? extends ResourceSupport> putItemResource(RootResourceInformation resourceInformation,
PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler,
ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader)
throws HttpRequestMethodNotSupportedException {
throws HttpRequestMethodNotSupportedException {
resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM);
@@ -406,7 +395,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
public ResponseEntity<ResourceSupport> patchItemResource(RootResourceInformation resourceInformation,
PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler,
ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader)
throws HttpRequestMethodNotSupportedException, ResourceNotFoundException {
throws HttpRequestMethodNotSupportedException, ResourceNotFoundException {
resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM);

View File

@@ -28,12 +28,14 @@ import java.util.Map.Entry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.core.util.Supplier;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.data.util.ClassTypeInformation;
@@ -53,9 +55,11 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
@@ -75,6 +79,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
private final RepositoryEntityLinks entityLinks;
private final ResourceMappings mappings;
private ResourceStatus resourceStatus;
/**
* Creates a new {@link RepositorySearchController} using the given {@link PagedResourcesAssembler},
@@ -86,7 +91,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
*/
@Autowired
public RepositorySearchController(PagedResourcesAssembler<Object> assembler, RepositoryEntityLinks entityLinks,
ResourceMappings mappings) {
ResourceMappings mappings, HttpHeadersPreparer headersPreparer) {
super(assembler);
@@ -95,6 +100,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
this.entityLinks = entityLinks;
this.mappings = mappings;
this.resourceStatus = ResourceStatus.of(headersPreparer);
}
/**
@@ -112,7 +118,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(Collections.singleton(HttpMethod.GET));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
return ResponseEntity.ok().headers(headers).build();
}
/**
@@ -126,7 +132,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
verifySearchesExposed(resourceInformation);
return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
return ResponseEntity.noContent().build();
}
/**
@@ -169,9 +175,9 @@ class RepositorySearchController extends AbstractRepositoryRestController {
*/
@ResponseBody
@RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET)
public ResponseEntity<Object> executeSearch(RootResourceInformation resourceInformation,
public ResponseEntity<?> executeSearch(RootResourceInformation resourceInformation,
@RequestParam MultiValueMap<String, Object> parameters, @PathVariable String search, DefaultedPageable pageable,
Sort sort, PersistentEntityResourceAssembler assembler) {
Sort sort, PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers) {
Method method = checkExecutability(resourceInformation, search);
Object result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, assembler);
@@ -180,7 +186,40 @@ class RepositorySearchController extends AbstractRepositoryRestController {
MethodResourceMapping methodMapping = searchMappings.getExportedMethodMappingForPath(search);
Class<?> domainType = methodMapping.getReturnedDomainType();
return new ResponseEntity<Object>(toResource(result, assembler, domainType, null), HttpStatus.OK);
return toResource(result, assembler, domainType, null, headers, resourceInformation);
}
/**
* Turns the given source into a {@link ResourceSupport} if needed and possible. Uses the given
* {@link PersistentEntityResourceAssembler} for the actual conversion.
*
* @param source can be must not be {@literal null}.
* @param assembler must not be {@literal null}.
* @param domainType the domain type in case the source is an empty iterable, must not be {@literal null}.
* @param baseLink can be {@literal null}.
* @return
*/
protected ResponseEntity<?> toResource(final Object source, final PersistentEntityResourceAssembler assembler,
Class<?> domainType, Link baseLink, HttpHeaders headers, RootResourceInformation information) {
if (source instanceof Iterable) {
return ResponseEntity.ok(toResources((Iterable<?>) source, assembler, domainType, baseLink));
} else if (source == null) {
throw new ResourceNotFoundException();
} else if (ClassUtils.isPrimitiveOrWrapper(source.getClass())) {
return ResponseEntity.ok(source);
}
PersistentEntity<?, ?> entity = information.getPersistentEntity();
return resourceStatus.getStatusAndHeaders(headers, source, entity).toResponseEntity(//
new Supplier<PersistentEntityResource>() {
@Override
public PersistentEntityResource get() {
return assembler.toFullResource(source);
}
});
}
/**
@@ -199,13 +238,17 @@ class RepositorySearchController extends AbstractRepositoryRestController {
@RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET, //
produces = { "application/x-spring-data-compact+json" })
public ResourceSupport executeSearchCompact(RootResourceInformation resourceInformation,
@RequestParam MultiValueMap<String, Object> parameters, @PathVariable String repository,
@PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) {
@RequestHeader HttpHeaders headers, @RequestParam MultiValueMap<String, Object> parameters,
@PathVariable String repository, @PathVariable String search, DefaultedPageable pageable, Sort sort,
PersistentEntityResourceAssembler assembler) {
Method method = checkExecutability(resourceInformation, search);
Object result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, assembler);
ResourceMetadata metadata = resourceInformation.getResourceMetadata();
Object resource = toResource(result, assembler, metadata.getDomainType(), null);
ResponseEntity<?> entity = toResource(result, assembler, metadata.getDomainType(), null, headers,
resourceInformation);
Object resource = entity.getBody();
List<Link> links = new ArrayList<Link>();
if (resource instanceof Resources && ((Resources<?>) resource).getContent() != null) {

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2016 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;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.rest.core.util.Supplier;
import org.springframework.data.rest.webmvc.support.ETag;
import org.springframework.hateoas.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* Simple abstraction to capture the status of a resource to determine whether it has been modified or not and produce
* {@link ResponseEntity} via its {@link StatusAndHeaders} sub component.
*
* @author Oliver Gierke
* @since 2.6
* @soundtrack Blumentopf - Dass ich nicht lache (Kein Zufall)
*/
@RequiredArgsConstructor(staticName = "of")
class ResourceStatus {
private final @NonNull HttpHeadersPreparer preparer;
/**
* Returns the {@link StatusAndHeaders} calculated from the given {@link HttpHeaders}, domain object and
* {@link RootResourceInformation.
*
* @param requestHeaders must not be {@literal null}.
* @param domainObject must not be {@literal null}.
* @param information
* @return
*/
public StatusAndHeaders getStatusAndHeaders(HttpHeaders requestHeaders, Object domainObject,
PersistentEntity<?, ?> entity) {
// Check ETag for If-Non-Match
List<String> ifNoneMatch = requestHeaders.getIfNoneMatch();
ETag eTag = ifNoneMatch.isEmpty() ? ETag.NO_ETAG : ETag.from(ifNoneMatch.get(0));
HttpHeaders responseHeaders = preparer.prepareHeaders(entity, domainObject);
// Check last modification for If-Modified-Since
return eTag.matches(entity, domainObject) || preparer.isObjectStillValid(domainObject, requestHeaders)
? StatusAndHeaders.notModified(responseHeaders) : StatusAndHeaders.modified(responseHeaders);
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class StatusAndHeaders {
private final @NonNull HttpHeaders headers;
private final @Getter(AccessLevel.PACKAGE) boolean modified;
private static StatusAndHeaders notModified(HttpHeaders headers) {
return new StatusAndHeaders(headers, false);
}
private static StatusAndHeaders modified(HttpHeaders headers) {
return new StatusAndHeaders(headers, true);
}
/**
* Creates a {@link ResponseEntity} based on the given {@link PersistentEntityResource}.
*
* @param supplier a {@link Supplier} to provide a {@link PersistentEntityResource} eventually, must not be
* {@literal null}.
* @return
*/
public ResponseEntity<Resource<?>> toResponseEntity(Supplier<PersistentEntityResource> supplier) {
return modified ? new ResponseEntity<Resource<?>>(supplier.get(), headers, HttpStatus.OK)
: new ResponseEntity<Resource<?>>(headers, HttpStatus.NOT_MODIFIED);
}
}
}

View File

@@ -77,6 +77,7 @@ import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.data.rest.webmvc.HttpHeadersPreparer;
import org.springframework.data.rest.webmvc.ProfileResourceProcessor;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestExceptionHandler;
@@ -773,6 +774,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return new MappingAuditableBeanWrapperFactory(persistentEntities());
}
@Bean
public HttpHeadersPreparer httpHeadersPreparer() {
return new HttpHeadersPreparer(auditableBeanWrapperFactory());
}
@Bean
public SelfLinkProvider selfLinkProvider() {
return new DefaultSelfLinkProvider(persistentEntities(), entityLinks(), getEntityLookups());

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2016 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;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import lombok.Value;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.annotation.Version;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.rest.core.util.Supplier;
import org.springframework.data.rest.webmvc.ResourceStatus.StatusAndHeaders;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/**
* Unit tests for {@link ResourceStatus}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class ResourceStatusUnitTests {
ResourceStatus status;
KeyValuePersistentEntity<?> entity;
@Mock HttpHeadersPreparer preparer;
@Mock Supplier<PersistentEntityResource> supplier;
@Before
public void setUp() {
this.status = ResourceStatus.of(preparer);
KeyValueMappingContext context = new KeyValueMappingContext();
this.entity = context.getPersistentEntity(Sample.class);
doReturn(new HttpHeaders()).when(preparer).prepareHeaders(eq(entity), Matchers.any());
}
/**
* @see DATAREST-835
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullPreparer() {
ResourceStatus.of(null);
}
/**
* @see DATAREST-835
*/
@Test
public void returnsModifiedIfNoHeadersGiven() {
assertModified(status.getStatusAndHeaders(new HttpHeaders(), new Sample(0), entity));
}
/**
* @see DATAREST-835
*/
@Test
public void returnsNotModifiedForEntityWithRequestedETag() {
HttpHeaders headers = new HttpHeaders();
headers.setIfNoneMatch("\"1\"");
assertNotModified(status.getStatusAndHeaders(headers, new Sample(1), entity));
}
/**
* @see DATAREST-835
*/
@Test
public void returnsNotModifiedIfEntityIsStillConsideredValid() {
doReturn(true).when(preparer).isObjectStillValid(Matchers.any(), Matchers.any(HttpHeaders.class));
assertNotModified(status.getStatusAndHeaders(new HttpHeaders(), new Sample(0), entity));
}
private void assertModified(StatusAndHeaders statusAndHeaders) {
assertThat(statusAndHeaders.isModified(), is(true));
assertThat(statusAndHeaders.toResponseEntity(supplier).getStatusCode(), is(HttpStatus.OK));
verify(supplier).get();
}
private void assertNotModified(StatusAndHeaders statusAndHeaders) {
assertThat(statusAndHeaders.isModified(), is(false));
assertThat(statusAndHeaders.toResponseEntity(supplier).getStatusCode(), is(HttpStatus.NOT_MODIFIED));
}
@Value
static class Sample {
@Version int version;
}
}