Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: always evaluate if-match headers when setting content, not just … #1797

Merged
merged 2 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,8 @@ protected void handleMultipart(HttpServletRequest request, HttpServletResponse r

boolean isNew = false;

if (target.exists()) {
HeaderUtils.evaluateHeaderConditions(headers, targetETag != null ? targetETag.toString() : null, new Date(resolveLastModified(target)));
} else {
HeaderUtils.evaluateHeaderConditions(headers, targetETag != null ? targetETag.toString() : null, new Date(resolveLastModified(target)));
if (target.exists() == false) {
isNew = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,19 @@ public long lastModified() throws IOException {

// TODO: can we remove this is all properties are effectively embedded?
Object lastModified = null; //BeanUtils.getFieldWithAnnotation(property, LastModifiedDate.class);
if (lastModified == null) {
if (lastModified == null && getDelegate() != null) {
return getDelegate().lastModified();
}

return Stream.of(lastModified)
.map(it -> getConversionService().convert(it, Date.class))//
.map(it -> getConversionService().convert(it, Instant.class))//
.map(it -> it.toEpochMilli())
.findFirst().orElseThrow(() -> new IllegalArgumentException(format("Invalid data type for @LastModifiedDate on Entity %s", this.getAssociation())));
if (lastModified != null) {
return Stream.of(lastModified)
.map(it -> getConversionService().convert(it, Date.class))//
.map(it -> getConversionService().convert(it, Instant.class))//
.map(it -> it.toEpochMilli())
.findFirst().orElseThrow(() -> new IllegalArgumentException(format("Invalid data type for @LastModifiedDate on Entity %s", this.getAssociation())));
}

return -1L;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.rest.config.HypermediaConfiguration;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.test.context.ActiveProfiles;
Expand Down Expand Up @@ -44,7 +45,8 @@
EntityConfig.class,
DelegatingWebMvcConfiguration.class,
RepositoryRestMvcConfiguration.class,
RestConfiguration.class })
RestConfiguration.class,
HypermediaConfiguration.class })
@Transactional
@ActiveProfiles("store")
public class BaseUriIT {
Expand Down Expand Up @@ -142,6 +144,8 @@ public class BaseUriIT {
version.setEntity(testEntity4);
version.setMvc(mvc);
version.setUrl(url);
version.setCollectionUrl("/api/testEntity4s");
version.setContentLinkRel("content");
version.setRepo(repo4);
version.setStore(store4);
version.setEtag(format("\"%s\"", testEntity4.getVersion()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.rest.config.HypermediaConfiguration;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.mock.web.MockHttpServletResponse;
Expand All @@ -41,7 +42,8 @@
EntityConfig.class,
DelegatingWebMvcConfiguration.class,
RepositoryRestMvcConfiguration.class,
RestConfiguration.class
RestConfiguration.class,
HypermediaConfiguration.class
})
@Transactional
@ActiveProfiles("store")
Expand Down Expand Up @@ -175,6 +177,8 @@ public class ContentEntityRestEndpointsIT {

version.setMvc(mvc);
version.setUrl(url);
version.setCollectionUrl("/testEntity4s");
version.setContentLinkRel("content");
version.setRepo(repo4);
version.setStore(store4);
version.setEtag(format("\"%s\"", testEntity4.getVersion()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.commons.property.PropertyPath;
import org.springframework.content.rest.config.HypermediaConfiguration;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.core.io.WritableResource;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
Expand All @@ -50,7 +51,9 @@
StoreConfig.class,
DelegatingWebMvcConfiguration.class,
RepositoryRestMvcConfiguration.class,
RestConfiguration.class })
RestConfiguration.class,
HypermediaConfiguration.class
})
@Transactional
@ActiveProfiles("store")
public class NestedContentPropertiesRestEndpointsIT {
Expand Down Expand Up @@ -171,6 +174,8 @@ public class NestedContentPropertiesRestEndpointsIT {

versionTests.setMvc(mvc);
versionTests.setUrl("/testEntity10s/" + testEntity10.getId() + "/child/content");
versionTests.setCollectionUrl("/testEntity10s");
versionTests.setContentLinkRel("child/content");
versionTests.setRepo(repository);
versionTests.setStore(store);
versionTests.setEtag(format("\"%s\"", testEntity10.getVersion()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.content.commons.property.PropertyPath;
import org.springframework.content.rest.config.HypermediaConfiguration;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.core.io.WritableResource;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
Expand Down Expand Up @@ -58,7 +59,8 @@
StoreConfig.class,
DelegatingWebMvcConfiguration.class,
RepositoryRestMvcConfiguration.class,
RestConfiguration.class })
RestConfiguration.class,
HypermediaConfiguration.class})
@Transactional
@ActiveProfiles("store")
public class NestedContentPropertyRestEndpointsIT {
Expand Down Expand Up @@ -194,6 +196,8 @@ public class NestedContentPropertyRestEndpointsIT {

versionTests.setMvc(mvc);
versionTests.setUrl("/testEntity8s/" + testEntity8.getId() + "/child");
versionTests.setCollectionUrl("/testEntity8s");
versionTests.setContentLinkRel("child");
versionTests.setRepo(repository2);
versionTests.setStore(store);
versionTests.setEtag(format("\"%s\"", testEntity8.getVersion()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public class RestResourceMappedRestEndpointsIT {

versionTests.setMvc(mvc);
versionTests.setUrl("/testEntity11s/" + testEntity11.getId() + "/package/content");
versionTests.setCollectionUrl("/testEntity11s");
versionTests.setContentLinkRel("package/content");
versionTests.setRepo(repo);
versionTests.setStore(store);
versionTests.setEtag(format("\"%s\"", testEntity11.getVersion()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package it.internal.org.springframework.content.rest.controllers;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Optional;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import internal.org.springframework.content.rest.support.ContentEntity;
import internal.org.springframework.content.rest.support.TestEntity2;
import internal.org.springframework.content.rest.support.TestEntity4;
Expand All @@ -25,10 +28,7 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand All @@ -37,6 +37,8 @@ public class Version {

private MockMvc mvc;
private String url;
private String collectionUrl;
private String contentLinkRel;
private CrudRepository repo;
private Store store;
private String etag;
Expand All @@ -47,6 +49,30 @@ public static Version tests() {
}

{
Context("#Issue 1975", () -> {
It("should always evaluate if-match header even after content deletion", () -> {
String entityUrl = mvc.perform(post(collectionUrl).content("{}"))
.andExpect(status().is2xxSuccessful()).andReturn().getResponse().getHeader("Location");
assertThat(entityUrl, is(not(nullValue())));

String body = mvc.perform(get(entityUrl).accept("application/json"))
.andExpect(status().is2xxSuccessful()).andReturn().getResponse().getContentAsString();

ObjectMapper objectMapper = new ObjectMapper();
JsonNode responseJson = objectMapper.readTree(body);
JsonNode linksNode = responseJson.get("_links");
JsonNode contentNode = linksNode.get(contentLinkRel);
String contentHref = contentNode.get("href").asText();

mvc.perform(put(contentHref).header("If-Match", "\"0\"").contentType("text/plain").content("Hello world!"))
.andExpect(status().is2xxSuccessful()); // Content created
mvc.perform(delete(contentHref).header("If-Match", "\"1\""))
.andExpect(status().is2xxSuccessful()); // User A
mvc.perform(put(contentHref).header("If-Match", "\"1\"").contentType("text/plain").content("foo bar"))
.andExpect(status().isPreconditionFailed()); // User B
});
});

Context("a GET request to /{store}/{id}", () -> {
It("should return an etag header", () -> {
MockHttpServletResponse response = mvc
Expand Down
Loading