JAVA-8363: Move etag code to spring-boot-mvc-3
This commit is contained in:
@@ -27,6 +27,14 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Version;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Entity
|
||||
public class Foo implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
public Foo() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Foo(final String name) {
|
||||
super();
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final Foo other = (Foo) obj;
|
||||
if (name == null) {
|
||||
if (other.name != null)
|
||||
return false;
|
||||
} else if (!name.equals(other.name))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("Foo [name=").append(name).append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = "/foos")
|
||||
public class FooController {
|
||||
|
||||
@Autowired
|
||||
private FooDao fooDao;
|
||||
|
||||
@GetMapping(value = "/{id}")
|
||||
public Foo findById(@PathVariable("id") final Long id, final HttpServletResponse response) {
|
||||
return fooDao.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
}
|
||||
|
||||
// Note: the global filter overrides the ETag value we set here. We can still
|
||||
// analyze its behaviour in the Integration Test.
|
||||
@GetMapping(value = "/{id}/custom-etag")
|
||||
public ResponseEntity<Foo> findByIdWithCustomEtag(@PathVariable("id") final Long id,
|
||||
final HttpServletResponse response) {
|
||||
final Foo foo = fooDao.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return ResponseEntity.ok().eTag(Long.toString(foo.getVersion())).body(foo);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public Foo create(@RequestBody final Foo resource, final HttpServletResponse response) {
|
||||
return fooDao.save(resource);
|
||||
}
|
||||
|
||||
@PutMapping(value = "/{id}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void update(@PathVariable("id") final Long id, @RequestBody final Foo resource) {
|
||||
fooDao.save(resource);
|
||||
}
|
||||
|
||||
@DeleteMapping(value = "/{id}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void delete(@PathVariable("id") final Long id) {
|
||||
fooDao.deleteById(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface FooDao extends CrudRepository<Foo, Long>{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringBootEtagApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringBootEtagApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig {
|
||||
|
||||
// Etags
|
||||
|
||||
// If we're not using Spring Boot we can make use of
|
||||
// AbstractAnnotationConfigDispatcherServletInitializer#getServletFilters
|
||||
@Bean
|
||||
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
|
||||
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
|
||||
filterRegistrationBean.addUrlPatterns("/foos/*");
|
||||
filterRegistrationBean.setName("etagFilter");
|
||||
return filterRegistrationBean;
|
||||
}
|
||||
|
||||
// We can also just declare the filter directly
|
||||
// @Bean
|
||||
// public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
|
||||
// return new ShallowEtagHeaderFilter();
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!-- NOTE: web.xml is not used in Spring Boot. This is just for guidance, showing how an Etag Filter would be implemented using XML-based configs -->
|
||||
|
||||
<!-- <?xml version="1.0" encoding="UTF-8"?> -->
|
||||
<!-- <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" -->
|
||||
<!-- xsi:schemaLocation=" -->
|
||||
<!-- http://java.sun.com/xml/ns/javaee -->
|
||||
<!-- http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0" -->
|
||||
<!-- > -->
|
||||
|
||||
<!-- <filter> -->
|
||||
<!-- <filter-name>etagFilter</filter-name> -->
|
||||
<!-- <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class> -->
|
||||
<!-- </filter> -->
|
||||
<!-- <filter-mapping> -->
|
||||
<!-- <filter-name>etagFilter</filter-name> -->
|
||||
<!-- <url-pattern>/*</url-pattern> -->
|
||||
<!-- </filter-mapping> -->
|
||||
<!-- </web-app> -->
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.baeldung.etag;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.response.Response;
|
||||
import org.assertj.core.util.Preconditions;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@ComponentScan(basePackageClasses = WebConfig.class)
|
||||
@EnableAutoConfiguration
|
||||
public class EtagIntegrationTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Test
|
||||
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
|
||||
// When
|
||||
final Response findOneResponse = RestAssured.given().header("Accept", "application/json").get(uriOfResource);
|
||||
|
||||
// Then
|
||||
assertNotNull(findOneResponse.getHeader(HttpHeaders.ETAG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
final Response findOneResponse = RestAssured.given().header("Accept", "application/json").get(uriOfResource);
|
||||
final String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
|
||||
|
||||
// When
|
||||
final Response secondFindOneResponse = RestAssured.given().header("Accept", "application/json")
|
||||
.headers("If-None-Match", etagValue).get(uriOfResource);
|
||||
|
||||
// Then
|
||||
assertTrue(secondFindOneResponse.getStatusCode() == 304);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
final Response firstFindOneResponse = RestAssured.given().header("Accept", "application/json")
|
||||
.get(uriOfResource);
|
||||
final String etagValue = firstFindOneResponse.getHeader(HttpHeaders.ETAG);
|
||||
final long createdId = firstFindOneResponse.jsonPath().getLong("id");
|
||||
|
||||
Foo updatedFoo = new Foo("updated value");
|
||||
updatedFoo.setId(createdId);
|
||||
Response updatedResponse = RestAssured.given().contentType(ContentType.JSON).body(updatedFoo)
|
||||
.put(uriOfResource);
|
||||
assertThat(updatedResponse.getStatusCode() == 200);
|
||||
|
||||
// When
|
||||
final Response secondFindOneResponse = RestAssured.given().header("Accept", "application/json")
|
||||
.headers("If-None-Match", etagValue).get(uriOfResource);
|
||||
|
||||
// Then
|
||||
assertTrue(secondFindOneResponse.getStatusCode() == 200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Not Yet Implemented By Spring - https://jira.springsource.org/browse/SPR-10164")
|
||||
public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
|
||||
// When
|
||||
final Response findOneResponse = RestAssured.given().header("Accept", "application/json")
|
||||
.headers("If-Match", randomAlphabetic(8)).get(uriOfResource);
|
||||
|
||||
// Then
|
||||
assertTrue(findOneResponse.getStatusCode() == 412);
|
||||
}
|
||||
|
||||
private final String createAsUri() {
|
||||
final Response response = createAsResponse(new Foo(randomAlphabetic(6)));
|
||||
Preconditions.checkState(response.getStatusCode() == 201, "create operation: " + response.getStatusCode());
|
||||
|
||||
return getURL() + "/" + response.getBody().as(Foo.class).getId();
|
||||
}
|
||||
|
||||
private Response createAsResponse(final Foo resource) {
|
||||
String resourceAsString;
|
||||
try {
|
||||
resourceAsString = new ObjectMapper().writeValueAsString(resource);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new AssertionError("Error during serialization");
|
||||
}
|
||||
return RestAssured.given().contentType(MediaType.APPLICATION_JSON.toString()).body(resourceAsString)
|
||||
.post(getURL());
|
||||
}
|
||||
|
||||
private String getURL() {
|
||||
return "http://localhost:" + port + "/foos";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user