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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Resource<?>> getItemResource(RootResourceInformation resourceInformation,
|
||||
@BackendId Serializable id, PersistentEntityResourceAssembler assembler)
|
||||
throws HttpRequestMethodNotSupportedException {
|
||||
@BackendId Serializable id, PersistentEntityResourceAssembler assembler,
|
||||
@RequestHeader MultiValueMap<String, String> rawHeaders) throws HttpRequestMethodNotSupportedException {
|
||||
|
||||
Object domainObj = getItemResource(resourceInformation, id);
|
||||
|
||||
@@ -310,10 +312,32 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
||||
return new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
PersistentEntityResource resource = assembler.toFullResource(domainObj);
|
||||
HttpHeaders headers = prepareHeaders(resource);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.putAll(rawHeaders);
|
||||
|
||||
return new ResponseEntity<Resource<?>>(resource, headers, HttpStatus.OK);
|
||||
// Check ETag for If-Non-Match
|
||||
|
||||
List<String> ifNoneMatch = headers.getIfNoneMatch();
|
||||
ETag eTag = ifNoneMatch.isEmpty() ? ETag.NO_ETAG : ETag.from(ifNoneMatch.get(0));
|
||||
|
||||
if (eTag.matches(resourceInformation.getPersistentEntity(), domainObj)) {
|
||||
return new ResponseEntity<Resource<?>>(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<Resource<?>>(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
PersistentEntityResource resource = assembler.toFullResource(domainObj);
|
||||
return new ResponseEntity<Resource<?>>(resource, prepareHeaders(resource), HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -246,6 +246,10 @@ public class PersistentEntityJackson2Module extends SimpleModule {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (persistentProperty.isVersionProperty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user