Merge pull request #6397 from rozagerardo/geroza/BAEL-11597_Move-and-update-etags-article
[BAEL-11597] Move and update Etags article
This commit is contained in:
commit
0f124ffd1c
|
@ -8,3 +8,4 @@ Module for the articles that are part of the Spring REST E-book:
|
|||
6. [REST API Discoverability and HATEOAS](http://www.baeldung.com/restful-web-service-discoverability)
|
||||
7. [Versioning a REST API](http://www.baeldung.com/rest-versioning)
|
||||
8. [Http Message Converters with the Spring Framework](http://www.baeldung.com/spring-httpmessageconverter-rest)
|
||||
9. [ETags for REST with Spring](http://www.baeldung.com/etags-for-rest-with-spring)
|
||||
|
|
|
@ -7,6 +7,7 @@ import javax.persistence.Entity;
|
|||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Version;
|
||||
|
||||
import com.thoughtworks.xstream.annotations.XStreamAlias;
|
||||
|
||||
|
@ -20,6 +21,9 @@ public class Foo implements Serializable {
|
|||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
public Foo() {
|
||||
super();
|
||||
|
@ -49,6 +53,14 @@ public class Foo implements Serializable {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.baeldung.spring;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
|
@ -9,6 +10,7 @@ import org.springframework.http.converter.HttpMessageConverter;
|
|||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
|
||||
import org.springframework.oxm.xstream.XStreamMarshaller;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
|
@ -34,16 +36,34 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
// }
|
||||
|
||||
// Another possibility is to create a bean which will be automatically added to the Spring Boot Autoconfigurations
|
||||
// @Bean
|
||||
// public HttpMessageConverter<Object> createXmlHttpMessageConverter() {
|
||||
// final MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();
|
||||
//
|
||||
// final XStreamMarshaller xstreamMarshaller = new XStreamMarshaller();
|
||||
// xstreamMarshaller.setAutodetectAnnotations(true);
|
||||
// xmlConverter.setMarshaller(xstreamMarshaller);
|
||||
// xmlConverter.setUnmarshaller(xstreamMarshaller);
|
||||
//
|
||||
// return xmlConverter;
|
||||
// }
|
||||
// @Bean
|
||||
// public HttpMessageConverter<Object> createXmlHttpMessageConverter() {
|
||||
// final MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();
|
||||
//
|
||||
// final XStreamMarshaller xstreamMarshaller = new XStreamMarshaller();
|
||||
// xstreamMarshaller.setAutodetectAnnotations(true);
|
||||
// xmlConverter.setMarshaller(xstreamMarshaller);
|
||||
// xmlConverter.setUnmarshaller(xstreamMarshaller);
|
||||
//
|
||||
// return xmlConverter;
|
||||
// }
|
||||
|
||||
// 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("/auth/foos/*");
|
||||
filterRegistrationBean.setName("etagFilter");
|
||||
return filterRegistrationBean;
|
||||
}
|
||||
|
||||
// We can also just declare the filter directly
|
||||
// @Bean
|
||||
// public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
|
||||
// return new ShallowEtagHeaderFilter();
|
||||
// }
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
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;
|
||||
|
@ -45,6 +46,18 @@ public class FooController {
|
|||
}
|
||||
|
||||
// API
|
||||
|
||||
// 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 resourceById = RestPreconditions.checkFound(service.findOne(id));
|
||||
|
||||
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
|
||||
return ResponseEntity.ok()
|
||||
.eTag(Long.toString(resourceById.getVersion()))
|
||||
.body(resourceById);
|
||||
}
|
||||
|
||||
// read - one
|
||||
|
||||
|
|
|
@ -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> -->
|
|
@ -1,21 +1,28 @@
|
|||
package com.baeldung.common.web;
|
||||
|
||||
import static com.baeldung.web.util.HTTPLinkHeaderUtil.extractURIByRel;
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.baeldung.persistence.model.Foo;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.response.Response;
|
||||
|
||||
public abstract class AbstractBasicLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
||||
|
@ -97,7 +104,82 @@ public abstract class AbstractBasicLiveTest<T extends Serializable> extends Abst
|
|||
final String uriToNextPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "next");
|
||||
assertNull(uriToNextPage);
|
||||
}
|
||||
|
||||
// etags
|
||||
|
||||
// count
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package com.baeldung.web;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import com.baeldung.persistence.model.Foo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
public class FooControllerCustomEtagIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
private String FOOS_ENDPOINT = "/auth/foos/";
|
||||
private String CUSTOM_ETAG_ENDPOINT_SUFFIX = "/custom-etag";
|
||||
|
||||
private static String serializeFoo(Foo foo) throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.writeValueAsString(foo);
|
||||
}
|
||||
|
||||
private static String createFooJson() throws Exception {
|
||||
return serializeFoo(new Foo(randomAlphabetic(6)));
|
||||
}
|
||||
|
||||
private static Foo deserializeFoo(String fooJson) throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readValue(fooJson, Foo.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceExists_whenRetrievingResourceUsingCustomEtagEndpoint_thenEtagIsAlsoReturned()
|
||||
throws Exception {
|
||||
// Given
|
||||
String createdResourceUri = this.mvc.perform(post(FOOS_ENDPOINT).contentType(MediaType.APPLICATION_JSON)
|
||||
.content(createFooJson()))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getHeader(HttpHeaders.LOCATION);
|
||||
|
||||
// When
|
||||
ResultActions result = this.mvc
|
||||
.perform(get(createdResourceUri + CUSTOM_ETAG_ENDPOINT_SUFFIX).contentType(MediaType.APPLICATION_JSON));
|
||||
|
||||
// Then
|
||||
result.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.ETAG, "\"0\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtagUsingCustomEtagEndpoint_thenNotModifiedReturned() throws Exception {
|
||||
// Given
|
||||
String createdResourceUri = this.mvc.perform(post(FOOS_ENDPOINT).contentType(MediaType.APPLICATION_JSON)
|
||||
.content(createFooJson()))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getHeader(HttpHeaders.LOCATION);
|
||||
ResultActions findOneResponse = this.mvc
|
||||
.perform(get(createdResourceUri + CUSTOM_ETAG_ENDPOINT_SUFFIX).contentType(MediaType.APPLICATION_JSON));
|
||||
String etag = findOneResponse.andReturn().getResponse().getHeader(HttpHeaders.ETAG);
|
||||
|
||||
// When
|
||||
ResultActions result = this.mvc
|
||||
.perform(get(createdResourceUri + CUSTOM_ETAG_ENDPOINT_SUFFIX).contentType(MediaType.APPLICATION_JSON).header(HttpHeaders.IF_NONE_MATCH, etag));
|
||||
|
||||
// Then
|
||||
result.andExpect(status().isNotModified());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtagUsingCustomEtagEndpoint_thenResourceIsReturned() throws Exception {
|
||||
// Given
|
||||
String createdResourceUri = this.mvc.perform(post(FOOS_ENDPOINT).contentType(MediaType.APPLICATION_JSON)
|
||||
.content(createFooJson()))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getHeader(HttpHeaders.LOCATION);
|
||||
ResultActions findOneResponse = this.mvc
|
||||
.perform(get(createdResourceUri + CUSTOM_ETAG_ENDPOINT_SUFFIX).contentType(MediaType.APPLICATION_JSON));
|
||||
String etag = findOneResponse.andReturn().getResponse().getHeader(HttpHeaders.ETAG);
|
||||
Foo createdFoo = deserializeFoo(findOneResponse.andReturn().getResponse().getContentAsString());
|
||||
createdFoo.setName("updated name");
|
||||
this.mvc
|
||||
.perform(put(createdResourceUri).contentType(MediaType.APPLICATION_JSON).content(serializeFoo(createdFoo)));
|
||||
|
||||
// When
|
||||
ResultActions result = this.mvc
|
||||
.perform(get(createdResourceUri + CUSTOM_ETAG_ENDPOINT_SUFFIX).contentType(MediaType.APPLICATION_JSON).header(HttpHeaders.IF_NONE_MATCH, etag));
|
||||
|
||||
// Then
|
||||
result.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.ETAG, "\"1\""));
|
||||
}
|
||||
|
||||
}
|
|
@ -8,8 +8,8 @@ import org.junit.runners.Suite;
|
|||
@Suite.SuiteClasses({
|
||||
// @formatter:off
|
||||
FooDiscoverabilityLiveTest.class,
|
||||
FooLiveTest.class
|
||||
,FooPageableLiveTest.class
|
||||
FooLiveTest.class,
|
||||
FooPageableLiveTest.class
|
||||
}) //
|
||||
public class LiveTestSuiteLiveTest {
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ The "REST With Spring" Classes: http://bit.ly/restwithspring
|
|||
The "Learn Spring Security" Classes: http://github.learnspringsecurity.com
|
||||
|
||||
### Relevant Articles:
|
||||
- [ETags for REST with Spring](http://www.baeldung.com/etags-for-rest-with-spring)
|
||||
- [Integration Testing with the Maven Cargo plugin](http://www.baeldung.com/integration-testing-with-the-maven-cargo-plugin)
|
||||
- [Introduction to Spring Data JPA](http://www.baeldung.com/the-persistence-layer-with-spring-data-jpa)
|
||||
- [Project Configuration with Spring](http://www.baeldung.com/project-configuration-with-spring)
|
||||
|
|
|
@ -8,11 +8,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.support.SpringBootServletInitializer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.context.request.RequestContextListener;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
|
||||
/**
|
||||
* Main Application Class - uses Spring Boot. Just run this as a normal Java
|
||||
|
@ -40,8 +38,4 @@ public class Application extends SpringBootServletInitializer {
|
|||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
|
||||
return new ShallowEtagHeaderFilter();
|
||||
}
|
||||
}
|
|
@ -6,10 +6,8 @@ import javax.servlet.http.HttpServletResponse;
|
|||
|
||||
import org.baeldung.persistence.model.Foo;
|
||||
import org.baeldung.persistence.service.IFooService;
|
||||
import org.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
||||
import org.baeldung.web.util.RestPreconditions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
@ -26,9 +24,6 @@ import com.google.common.base.Preconditions;
|
|||
@RequestMapping(value = "/auth/foos")
|
||||
public class FooController {
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired
|
||||
private IFooService service;
|
||||
|
||||
|
@ -71,9 +66,6 @@ public class FooController {
|
|||
public Foo create(@RequestBody final Foo resource, final HttpServletResponse response) {
|
||||
Preconditions.checkNotNull(resource);
|
||||
final Foo foo = service.create(resource);
|
||||
final Long idOfCreatedResource = foo.getId();
|
||||
|
||||
eventPublisher.publishEvent(new ResourceCreatedEvent(this, response, idOfCreatedResource));
|
||||
|
||||
return foo;
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package org.baeldung.web.hateoas.event;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class ResourceCreatedEvent extends ApplicationEvent {
|
||||
private final HttpServletResponse response;
|
||||
private final long idOfNewResource;
|
||||
|
||||
public ResourceCreatedEvent(final Object source, final HttpServletResponse response, final long idOfNewResource) {
|
||||
super(source);
|
||||
|
||||
this.response = response;
|
||||
this.idOfNewResource = idOfNewResource;
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
public HttpServletResponse getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
public long getIdOfNewResource() {
|
||||
return idOfNewResource;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package org.baeldung.web.hateoas.listener;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
@Component
|
||||
class ResourceCreatedDiscoverabilityListener implements ApplicationListener<ResourceCreatedEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(final ResourceCreatedEvent resourceCreatedEvent) {
|
||||
Preconditions.checkNotNull(resourceCreatedEvent);
|
||||
|
||||
final HttpServletResponse response = resourceCreatedEvent.getResponse();
|
||||
final long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();
|
||||
|
||||
addLinkHeaderOnResourceCreation(response, idOfNewResource);
|
||||
}
|
||||
|
||||
void addLinkHeaderOnResourceCreation(final HttpServletResponse response, final long idOfNewResource) {
|
||||
// final String requestUrl = request.getRequestURL().toString();
|
||||
// final URI uri = new UriTemplate("{requestUrl}/{idOfNewResource}").expand(requestUrl, idOfNewResource);
|
||||
|
||||
final URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
|
||||
response.setHeader(HttpHeaders.LOCATION, uri.toASCIIString());
|
||||
}
|
||||
|
||||
}
|
|
@ -23,15 +23,6 @@
|
|||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Spring child -->
|
||||
<servlet>
|
||||
<servlet-name>api</servlet-name>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package org.baeldung;
|
||||
|
||||
import org.baeldung.persistence.PersistenceTestSuite;
|
||||
import org.baeldung.web.LiveTestSuiteLiveTest;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses({
|
||||
// @formatter:off
|
||||
PersistenceTestSuite.class
|
||||
,LiveTestSuiteLiveTest.class
|
||||
}) //
|
||||
public class TestSuiteLiveTest {
|
||||
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package org.baeldung.common.web;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
|
||||
public abstract class AbstractBasicLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
||||
|
||||
public AbstractBasicLiveTest(final Class<T> clazzToSet) {
|
||||
super(clazzToSet);
|
||||
}
|
||||
|
||||
// tests
|
||||
|
||||
@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
|
||||
@Ignore("No Update operation yet")
|
||||
public void givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
final Response findOneResponse = RestAssured.given()
|
||||
.header("Accept", "application/json")
|
||||
.get(uriOfResource);
|
||||
final String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
|
||||
|
||||
// existingResource.setName(randomAlphabetic(6));
|
||||
// getApi().update(existingResource.setName("randomString"));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// find - one
|
||||
|
||||
// find - all
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package org.baeldung.web;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
|
||||
import org.baeldung.common.web.AbstractBasicLiveTest;
|
||||
import org.baeldung.persistence.model.Foo;
|
||||
import org.baeldung.spring.ConfigIntegrationTest;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = { ConfigIntegrationTest.class }, loader = AnnotationConfigContextLoader.class)
|
||||
@ActiveProfiles("test")
|
||||
public class FooLiveTest extends AbstractBasicLiveTest<Foo> {
|
||||
|
||||
public FooLiveTest() {
|
||||
super(Foo.class);
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
@Override
|
||||
public final void create() {
|
||||
create(new Foo(randomAlphabetic(6)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String createAsUri() {
|
||||
return createAsUri(new Foo(randomAlphabetic(6)));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.baeldung.web;
|
||||
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses({
|
||||
// @formatter:off
|
||||
FooLiveTest.class
|
||||
}) //
|
||||
public class LiveTestSuiteLiveTest {
|
||||
|
||||
}
|
Loading…
Reference in New Issue