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:
@@ -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();
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user