DATAREST-471 - Populate Last-Modified headers from auditing information.

We now populate the Last-Modified header for requests to the item resource and search resource invocations that return a single item.
This commit is contained in:
Oliver Gierke
2015-02-04 21:03:57 +01:00
parent 14cee9c94f
commit ec2bd0145d
10 changed files with 154 additions and 54 deletions

View File

@@ -18,16 +18,22 @@ package org.springframework.data.rest.webmvc;
import static org.springframework.data.rest.webmvc.ControllerUtils.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Calendar;
import java.util.List;
import org.springframework.data.auditing.AuditableBeanWrapper;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.support.ETag;
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.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* @author Jon Brisbin
@@ -38,16 +44,23 @@ import org.springframework.util.Assert;
class AbstractRepositoryRestController {
private final PagedResourcesAssembler<Object> pagedResourcesAssembler;
private final AuditableBeanWrapperFactory auditableBeanWrapperFactory;
/**
* Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler}.
* Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler} and
* {@link AuditableBeanWrapperFactory}.
*
* @param pagedResourcesAssembler must not be {@literal null}.
* @param auditableBeanWrapperFactory must not be {@literal null}.
*/
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> pagedResourcesAssembler) {
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> pagedResourcesAssembler,
AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null!");
Assert.notNull(auditableBeanWrapperFactory, "AuditableBeanWrapperFactory must not be null!");
this.pagedResourcesAssembler = pagedResourcesAssembler;
this.auditableBeanWrapperFactory = auditableBeanWrapperFactory;
}
protected Link resourceLink(RootResourceInformation resourceLink, Resource resource) {
@@ -61,18 +74,35 @@ class AbstractRepositoryRestController {
}
@SuppressWarnings({ "unchecked" })
protected Resources resultToResources(Object result, PersistentEntityResourceAssembler assembler, Link baseLink) {
protected Resources<?> toResources(Iterable<?> source, PersistentEntityResourceAssembler assembler, Link baseLink) {
if (result instanceof Page) {
Page<Object> page = (Page<Object>) result;
if (source instanceof Page) {
Page<Object> page = (Page<Object>) source;
return entitiesToResources(page, assembler, baseLink);
} else if (result instanceof Iterable) {
return entitiesToResources((Iterable<Object>) result, assembler);
} else if (null == result) {
return new Resources(EMPTY_RESOURCE_LIST);
} else if (source instanceof Iterable) {
return entitiesToResources((Iterable<Object>) source, assembler);
} else {
Resource<Object> resource = assembler.toFullResource(result);
return new Resources(Collections.singletonList(resource));
return new Resources(EMPTY_RESOURCE_LIST);
}
}
/**
* 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 baseLink can be {@literal null}.
* @return
*/
protected Object toResource(Object source, PersistentEntityResourceAssembler assembler, Link baseLink) {
if (source instanceof Iterable) {
return toResources((Iterable<?>) source, assembler, baseLink);
} else if (source == null || ClassUtils.isPrimitiveOrWrapper(source.getClass())) {
return source;
} else {
return assembler.toFullResource(source);
}
}
@@ -93,4 +123,38 @@ class AbstractRepositoryRestController {
return new Resources<Resource<Object>>(resources);
}
/**
* Returns the default headers to be returned for the given {@link PersistentEntityResource}. Will set {@link ETag}
* and {@code Last-Modified} headers if applicable.
*
* @param resource can be {@literal null}.
* @return
*/
protected HttpHeaders prepareHeaders(PersistentEntityResource resource) {
HttpHeaders headers = new HttpHeaders();
if (resource == null) {
return headers;
}
// Add ETag
headers = ETag.from(resource).addTo(headers);
// Add Last-Modified
AuditableBeanWrapper wrapper = auditableBeanWrapperFactory.getBeanWrapperFor(resource.getContent());
if (wrapper == null) {
return headers;
}
Calendar lastModifiedDate = wrapper.getLastModifiedDate();
if (lastModifiedDate != null) {
headers.setLastModified(lastModifiedDate.getTimeInMillis());
}
return headers;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.rest.webmvc;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
@@ -53,12 +54,13 @@ public class RepositoryController extends AbstractRepositoryRestController {
* @param repositories must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param mappings must not be {@literal null}.
* @param auditableBeanWrapperFactory must not be {@literal null}.
*/
@Autowired
public RepositoryController(PagedResourcesAssembler<Object> assembler, Repositories repositories,
EntityLinks entityLinks, ResourceMappings mappings) {
EntityLinks entityLinks, ResourceMappings mappings, AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
super(assembler);
super(assembler, auditableBeanWrapperFactory);
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@@ -89,9 +90,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
@Autowired
public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config,
RepositoryEntityLinks entityLinks, PagedResourcesAssembler<Object> assembler,
@Qualifier("defaultConversionService") ConversionService conversionService) {
@Qualifier("defaultConversionService") ConversionService conversionService,
AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
super(assembler);
super(assembler, auditableBeanWrapperFactory);
this.entityLinks = entityLinks;
this.config = config;
@@ -193,9 +195,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(), pageable.isDefault() ? null
: pageable.getPageable());
Resources<?> resources = resultToResources(results, assembler, baseLink);
resources.add(links);
return resources;
Resources<?> result = toResources(results, assembler, baseLink);
result.add(links);
return result;
}
@ResponseBody
@@ -268,14 +270,18 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.HEAD)
public ResponseEntity<?> headForItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id)
throws HttpRequestMethodNotSupportedException {
public ResponseEntity<?> headForItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id,
PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException {
if (getItemResource(resourceInformation, id) != null) {
return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
Object domainObject = getItemResource(resourceInformation, id);
if (domainObject == null) {
throw new ResourceNotFoundException();
}
throw new ResourceNotFoundException();
PersistentEntityResource resource = assembler.toResource(domainObject);
return new ResponseEntity<Object>(prepareHeaders(resource), HttpStatus.NO_CONTENT);
}
/**
@@ -298,7 +304,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
}
PersistentEntityResource resource = assembler.toFullResource(domainObj);
HttpHeaders headers = ETag.from(resource).addTo(new HttpHeaders());
HttpHeaders headers = prepareHeaders(resource);
return new ResponseEntity<Resource<?>>(resource, headers, HttpStatus.OK);
}
@@ -415,17 +421,14 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
Object obj = invoker.invokeSave(domainObject);
publisher.publishEvent(new AfterSaveEvent(domainObject));
HttpHeaders headers = new HttpHeaders();
PersistentEntityResource resource = assembler.toFullResource(obj);
HttpHeaders headers = prepareHeaders(resource);
if (PUT.equals(httpMethod)) {
addLocationHeader(headers, assembler, obj);
}
if (config.isReturnBodyOnUpdate()) {
PersistentEntityResource resource = assembler.toFullResource(obj);
headers = ETag.from(resource).addTo(headers);
return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, resource);
} else {
return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers);
@@ -446,11 +449,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
Object savedObject = invoker.invokeSave(domainObject);
publisher.publishEvent(new AfterCreateEvent(savedObject));
HttpHeaders headers = new HttpHeaders();
addLocationHeader(headers, assembler, savedObject);
PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toFullResource(savedObject) : null;
headers = ETag.from(resource).addTo(headers);
HttpHeaders headers = prepareHeaders(resource);
addLocationHeader(headers, assembler, savedObject);
return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource);
}

View File

@@ -33,6 +33,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@@ -85,9 +86,9 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
@Autowired
public RepositoryPropertyReferenceController(Repositories repositories,
@Qualifier("defaultConversionService") ConversionService conversionService,
PagedResourcesAssembler<Object> assembler) {
PagedResourcesAssembler<Object> assembler, AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
super(assembler);
super(assembler, auditableBeanWrapperFactory);
this.repositories = repositories;
this.conversionService = conversionService;

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -44,7 +45,6 @@ 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.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -78,9 +78,10 @@ class RepositorySearchController extends AbstractRepositoryRestController {
*/
@Autowired
public RepositorySearchController(PagedResourcesAssembler<Object> assembler, RepositoryEntityLinks entityLinks,
ResourceMappings mappings, HateoasSortHandlerMethodArgumentResolver sortResolver) {
ResourceMappings mappings, HateoasSortHandlerMethodArgumentResolver sortResolver,
AuditableBeanWrapperFactory auditableBeanWrapperFactory) {
super(assembler);
super(assembler, auditableBeanWrapperFactory);
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
@@ -165,9 +166,9 @@ class RepositorySearchController extends AbstractRepositoryRestController {
@PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) {
Method method = checkExecutability(resourceInformation, search);
Object resources = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler);
Object result = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler);
return new ResponseEntity<Object>(resources, HttpStatus.OK);
return new ResponseEntity<Object>(toResource(result, assembler, null), HttpStatus.OK);
}
/**
@@ -187,8 +188,8 @@ class RepositorySearchController extends AbstractRepositoryRestController {
PersistentEntityResourceAssembler assembler) {
Method method = checkExecutability(resourceInformation, search);
Object resource = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler);
Object result = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler);
Object resource = toResource(result, assembler, null);
List<Link> links = new ArrayList<Link>();
if (resource instanceof Resources && ((Resources<?>) resource).getContent() != null) {
@@ -275,13 +276,7 @@ class RepositorySearchController extends AbstractRepositoryRestController {
DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) {
Map<String, String[]> parameters = request.getParameterMap();
Object result = invoker.invokeQueryMethod(method, parameters, pageable.getPageable(), sort);
if (ClassUtils.isPrimitiveOrWrapper(method.getReturnType())) {
return result;
}
return resultToResources(result, assembler, null);
return invoker.invokeQueryMethod(method, parameters, pageable.getPageable(), sort);
}
/**

View File

@@ -42,6 +42,8 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.auditing.AuditableBeanWrapperFactory;
import org.springframework.data.auditing.MappingAuditableBeanWrapperFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.geo.GeoModule;
import org.springframework.data.mapping.context.MappingContext;
@@ -629,6 +631,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
return OrderAwarePluginRegistry.create(converters);
}
@Bean
public AuditableBeanWrapperFactory auditableBeanWrapperFactory() {
return new MappingAuditableBeanWrapperFactory(persistentEntities());
}
protected List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers() {
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2015 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.
@@ -121,7 +121,9 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
Address address = repository.save(new Address());
ResponseEntity<?> entity = controller.headForItemResource(getResourceInformation(Address.class), address.id);
ResponseEntity<?> entity = controller.headForItemResource(getResourceInformation(Address.class), address.id,
assembler);
assertThat(entity.getStatusCode(), is(HttpStatus.NO_CONTENT));
}
@@ -130,7 +132,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
*/
@Test(expected = ResourceNotFoundException.class)
public void doesNotExposeHeadForItemResourceIfNotExisting() throws Exception {
controller.headForItemResource(getResourceInformation(CreditCard.class), 1L);
controller.headForItemResource(getResourceInformation(CreditCard.class), 1L, assembler);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.mongodb;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
@@ -24,9 +25,11 @@ import com.mongodb.MongoClient;
/**
* @author Jon Brisbin
* @author Oliver Gierke
*/
@Configuration
@EnableMongoRepositories
@EnableMongoAuditing
public class MongoDbRepositoryConfig extends AbstractMongoConfiguration {
/*

View File

@@ -186,4 +186,18 @@ public class MongoWebTests extends CommonWebTests {
.contentType(MediaType.APPLICATION_JSON).header("If-Match", concurrencyTag)).andExpect(
status().isPreconditionFailed());
}
/**
* @see DATAREST-471
*/
@Test
public void auditableResourceHasLastModifiedHeaderSet() throws Exception {
Profile profile = repository.findAll().iterator().next();
String header = mvc.perform(get("/profiles/{id}", profile.getId())).//
andReturn().getResponse().getHeader("Last-Modified");
assertThat(header, not(isEmptyOrNullString()));
}
}

View File

@@ -1,8 +1,13 @@
package org.springframework.data.rest.webmvc.mongodb;
import java.util.Date;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.Document;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author Jon Brisbin
*/
@@ -12,6 +17,7 @@ public class Profile {
@Id private String id;
private Long person;
private String type;
private @LastModifiedDate Date lastModifiedDate;
public String getId() {
return id;
@@ -40,4 +46,8 @@ public class Profile {
return this;
}
@JsonIgnore
public Date getLastModifiedDate() {
return lastModifiedDate;
}
}