diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java index 438705ffc..bde1f5a5f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -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 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 pagedResourcesAssembler) { + public AbstractRepositoryRestController(PagedResourcesAssembler 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 page = (Page) result; + if (source instanceof Page) { + Page page = (Page) source; return entitiesToResources(page, assembler, baseLink); - } else if (result instanceof Iterable) { - return entitiesToResources((Iterable) result, assembler); - } else if (null == result) { - return new Resources(EMPTY_RESOURCE_LIST); + } else if (source instanceof Iterable) { + return entitiesToResources((Iterable) source, assembler); } else { - Resource 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>(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; + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index 811328d02..36cea5f3e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -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 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!"); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 4180c0488..7801779f9 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -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 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(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(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, 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); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index ef9a6316e..ab33ef1df 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -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 assembler) { + PagedResourcesAssembler assembler, AuditableBeanWrapperFactory auditableBeanWrapperFactory) { - super(assembler); + super(assembler, auditableBeanWrapperFactory); this.repositories = repositories; this.conversionService = conversionService; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index fbc8e14c5..00231091f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -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 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(resources, HttpStatus.OK); + return new ResponseEntity(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 links = new ArrayList(); if (resource instanceof Resources && ((Resources) resource).getContent() != null) { @@ -275,13 +276,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { Map 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); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 5baa5787f..955972a1c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -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 defaultMethodArgumentResolvers() { SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 8c63d4444..21de8f0e6 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -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); } /** diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java index 0330be696..db016f9eb 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoDbRepositoryConfig.java @@ -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 { /* diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java index 29f3e1413..d8985742e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java @@ -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())); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Profile.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Profile.java index 595d9e4ee..e2589c024 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Profile.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Profile.java @@ -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; + } }