From 7d44b1ecabea8ac92c3a9ed07d8f416172b63a7d Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 8 Apr 2015 14:41:58 +0200 Subject: [PATCH] DATAREST-506 - Support for conditional GETs on item resources. We now inspect If-None-Match and If-Modified-Since headers on GET requests to item resources and return 304 Not Modified if appropriate. Disable rendering of version property in Jackson serializer as it's reflected in the ETag. Related tickets: DATAREST-160, DATAREST-471. --- .../AbstractRepositoryRestController.java | 14 +++++- .../webmvc/RepositoryEntityController.java | 36 ++++++++++--- .../json/PersistentEntityJackson2Module.java | 4 ++ .../data/rest/webmvc/support/ETag.java | 19 ++++++- .../rest/webmvc/mongodb/MongoWebTests.java | 28 ++++++++++- .../data/rest/webmvc/mongodb/Receipt.java | 50 +++---------------- 6 files changed, 98 insertions(+), 53 deletions(-) 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 bde1f5a5f..ab695aeeb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 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. @@ -143,7 +143,7 @@ class AbstractRepositoryRestController { headers = ETag.from(resource).addTo(headers); // Add Last-Modified - AuditableBeanWrapper wrapper = auditableBeanWrapperFactory.getBeanWrapperFor(resource.getContent()); + AuditableBeanWrapper wrapper = getAuditableBeanWrapper(resource.getContent()); if (wrapper == null) { return headers; @@ -157,4 +157,14 @@ class AbstractRepositoryRestController { return headers; } + + /** + * Returns the {@link AuditableBeanWrapper} for the given source. + * + * @param source can be {@literal null}. + * @return + */ + protected AuditableBeanWrapper getAuditableBeanWrapper(Object source) { + return auditableBeanWrapperFactory.getBeanWrapperFor(source); + } } 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 f3417db12..2c439f852 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -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.AuditableBeanWrapper; import org.springframework.data.auditing.AuditableBeanWrapperFactory; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -62,6 +63,7 @@ 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; @@ -301,8 +303,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity> getItemResource(RootResourceInformation resourceInformation, - @BackendId Serializable id, PersistentEntityResourceAssembler assembler) - throws HttpRequestMethodNotSupportedException { + @BackendId Serializable id, PersistentEntityResourceAssembler assembler, + @RequestHeader MultiValueMap rawHeaders) throws HttpRequestMethodNotSupportedException { Object domainObj = getItemResource(resourceInformation, id); @@ -310,10 +312,32 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem return new ResponseEntity>(HttpStatus.NOT_FOUND); } - PersistentEntityResource resource = assembler.toFullResource(domainObj); - HttpHeaders headers = prepareHeaders(resource); + HttpHeaders headers = new HttpHeaders(); + headers.putAll(rawHeaders); - return new ResponseEntity>(resource, headers, HttpStatus.OK); + // Check ETag for If-Non-Match + + List ifNoneMatch = headers.getIfNoneMatch(); + ETag eTag = ifNoneMatch.isEmpty() ? ETag.NO_ETAG : ETag.from(ifNoneMatch.get(0)); + + if (eTag.matches(resourceInformation.getPersistentEntity(), domainObj)) { + return new ResponseEntity>(HttpStatus.NOT_MODIFIED); + } + + // Check last modification for If-Modfied-Since + + if (headers.getIfModifiedSince() != -1) { + + AuditableBeanWrapper wrapper = getAuditableBeanWrapper(domainObj); + long current = wrapper.getLastModifiedDate().getTimeInMillis() / 1000 * 1000; + + if (current <= headers.getIfModifiedSince()) { + return new ResponseEntity>(HttpStatus.NOT_MODIFIED); + } + } + + PersistentEntityResource resource = assembler.toFullResource(domainObj); + return new ResponseEntity>(resource, prepareHeaders(resource), HttpStatus.OK); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index ea314b45d..0550f6c7c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -246,6 +246,10 @@ public class PersistentEntityJackson2Module extends SimpleModule { continue; } + if (persistentProperty.isVersionProperty()) { + continue; + } + result.add(writer); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ETag.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ETag.java index 38917b4db..de211824b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ETag.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/ETag.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. @@ -85,6 +85,23 @@ public final class ETag { } } + /** + * Returns whether the {@link ETag} matches the given {@link PersistentEntity} and target. A more dissenting way of + * checking matches as it does not match if the ETag is {@link #NO_ETAG}. + * + * @param entity must not be {@literal null}. + * @param target can be {@literal null}. + * @return + */ + public boolean matches(PersistentEntity entity, Object target) { + + if (this == NO_ETAG || target == null) { + return false; + } + + return this.equals(from(entity, target)); + } + /** * Adds the current {@link ETag} to the given headers. * 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 32c164fa9..90f18c55e 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 @@ -178,8 +178,8 @@ public class MongoWebTests extends CommonWebTests { Link receiptLink = client.discoverUnique("receipts"); Receipt receipt = new Receipt(); - receipt.setAmount(new BigDecimal(50)); - receipt.setSaleItem("Springy Tacos"); + receipt.amount = new BigDecimal(50); + receipt.saleItem = "Springy Tacos"; String stringReceipt = mapper.writeValueAsString(receipt); @@ -274,4 +274,28 @@ public class MongoWebTests extends CommonWebTests { client.follow(profileLink).andExpect(jsonPath("$.metadata.Key").value("Value")); } + + /** + * @see DATAREST-506 + */ + @Test + public void supportsConditionalGetsOnItemResource() throws Exception { + + Receipt receipt = new Receipt(); + receipt.amount = new BigDecimal(50); + receipt.saleItem = "Springy Tacos"; + + Link receiptsLink = client.discoverUnique("receipts"); + + MockHttpServletResponse response = postAndGet(receiptsLink, mapper.writeValueAsString(receipt), + MediaType.APPLICATION_JSON); + + Link receiptLink = client.getDiscoverer(response).findLinkWithRel("self", response.getContentAsString()); + + mvc.perform(get(receiptLink.getHref()).header("If-Modified-Since", response.getHeader("Last-Modified"))).// + andExpect(status().isNotModified()); + + mvc.perform(get(receiptLink.getHref()).header("If-None-Match", response.getHeader("ETag"))).// + andExpect(status().isNotModified()); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Receipt.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Receipt.java index f617e0e40..ad5d568db 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Receipt.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/Receipt.java @@ -16,59 +16,25 @@ package org.springframework.data.rest.webmvc.mongodb; +import java.math.BigDecimal; +import java.util.Date; 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 java.math.BigDecimal; - - /** * @author Pablo Lozano */ @Document public class Receipt { - @Id - public String id; + public @Id String id; + public @Version Long version; + public @LastModifiedDate Date date; - private String saleItem; + public String saleItem; + public BigDecimal amount; - private BigDecimal amount; - - @Version - private Long version; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getSaleItem() { - return saleItem; - } - - public void setSaleItem(String saleItem) { - this.saleItem = saleItem; - } - - public BigDecimal getAmount() { - return amount; - } - - public void setAmount(BigDecimal amount) { - this.amount = amount; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } }