Merge pull request #8917 from aurasphere/JAVA-22_spring_boot_rest_refactoring
[JAVA-22] Moved articles not in ebook from spring-boot-rest to spring…
This commit is contained in:
commit
eae0791dcb
|
@ -7,4 +7,8 @@ This module contains articles about Spring Web MVC in Spring Boot projects.
|
|||
- [Functional Controllers in Spring MVC](https://www.baeldung.com/spring-mvc-functional-controllers)
|
||||
- [Specify an Array of Strings as Body Parameters in Swagger](https://www.baeldung.com/swagger-body-array-of-strings)
|
||||
- [Swagger @ApiParam vs @ApiModelProperty](https://www.baeldung.com/swagger-apiparam-vs-apimodelproperty)
|
||||
- [ETags for REST with Spring](https://www.baeldung.com/etags-for-rest-with-spring)
|
||||
- [Testing REST with multiple MIME types](https://www.baeldung.com/testing-rest-api-with-multiple-media-types)
|
||||
- [Testing Web APIs with Postman Collections](https://www.baeldung.com/postman-testing-collections)
|
||||
- [Spring Boot Consuming and Producing JSON](https://www.baeldung.com/spring-boot-json)
|
||||
- More articles: [[prev -->]](/spring-boot-modules/spring-boot-mvc)
|
||||
|
|
|
@ -49,6 +49,27 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.thoughtworks.xstream</groupId>
|
||||
<artifactId>xstream</artifactId>
|
||||
<version>${xstream.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
@ -107,6 +128,7 @@
|
|||
<start-class>com.baeldung.swagger2boot.SpringBootSwaggerApplication</start-class>
|
||||
<!-- <start-class>com.baeldung.springbootmvc.SpringBootMvcFnApplication</start-class> -->
|
||||
<spring-boot.version>2.2.0.BUILD-SNAPSHOT</spring-boot.version>
|
||||
<xstream.version>1.4.11.1</xstream.version>
|
||||
</properties>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,95 @@
|
|||
package com.baeldung.etag;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Version;
|
||||
|
||||
@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 javax.servlet.http.HttpServletResponse;
|
||||
|
||||
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;
|
||||
|
||||
@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,13 @@
|
|||
package com.baeldung.students;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringBootStudentsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringBootStudentsApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.baeldung.students;
|
||||
|
||||
public class Student {
|
||||
|
||||
private long id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
public Student() {}
|
||||
|
||||
public Student(String firstName, String lastName) {
|
||||
super();
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public Student(long id, String firstName, String lastName) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Student [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.baeldung.students;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RestController;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import com.baeldung.students.StudentService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/students")
|
||||
public class StudentController {
|
||||
|
||||
@Autowired
|
||||
private StudentService service;
|
||||
|
||||
@GetMapping("/")
|
||||
public List<Student> read() {
|
||||
return service.readAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Student> read(@PathVariable("id") Long id) {
|
||||
Student foundStudent = service.read(id);
|
||||
if (foundStudent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
} else {
|
||||
return ResponseEntity.ok(foundStudent);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/")
|
||||
public ResponseEntity<Student> create(@RequestBody Student student) throws URISyntaxException {
|
||||
Student createdStudent = service.create(student);
|
||||
|
||||
URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
|
||||
.path("/{id}")
|
||||
.buildAndExpand(createdStudent.getId())
|
||||
.toUri();
|
||||
|
||||
return ResponseEntity.created(uri)
|
||||
.body(createdStudent);
|
||||
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Student> update(@RequestBody Student student, @PathVariable Long id) {
|
||||
Student updatedStudent = service.update(id, student);
|
||||
if (updatedStudent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
} else {
|
||||
return ResponseEntity.ok(updatedStudent);
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Object> deleteStudent(@PathVariable Long id) {
|
||||
service.delete(id);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.baeldung.students;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class StudentService {
|
||||
|
||||
// DB repository mock
|
||||
private Map<Long, Student> repository = Arrays.asList(
|
||||
new Student[]{
|
||||
new Student(1, "Alan","Turing"),
|
||||
new Student(2, "Sebastian","Bach"),
|
||||
new Student(3, "Pablo","Picasso"),
|
||||
}).stream()
|
||||
.collect(Collectors.toConcurrentMap(s -> s.getId(), Function.identity()));
|
||||
|
||||
// DB id sequence mock
|
||||
private AtomicLong sequence = new AtomicLong(3);
|
||||
|
||||
public List<Student> readAll() {
|
||||
return repository.values().stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Student read(Long id) {
|
||||
return repository.get(id);
|
||||
}
|
||||
|
||||
public Student create(Student student) {
|
||||
long key = sequence.incrementAndGet();
|
||||
student.setId(key);
|
||||
repository.put(key, student);
|
||||
return student;
|
||||
}
|
||||
|
||||
public Student update(Long id, Student student) {
|
||||
student.setId(id);
|
||||
Student oldStudent = repository.replace(id, student);
|
||||
return oldStudent == null ? null : student;
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
repository.remove(id);
|
||||
}
|
||||
}
|
|
@ -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,180 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "9989b5be-13ba-4d22-8e43-d05dbf628e58",
|
||||
"name": "foo API test",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "add a foo",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "a01534dc-6fc7-4c54-ba1d-6bcf311e5836",
|
||||
"exec": [
|
||||
"pm.test(\"success status\", () => pm.response.to.be.success );",
|
||||
"",
|
||||
"pm.test(\"name is correct\", () => ",
|
||||
" pm.expect(pm.response.json().name).to.equal(\"Transformers\"));",
|
||||
"",
|
||||
"pm.test(\"id was assigned\", () => ",
|
||||
" pm.expect(pm.response.json().id).to.be.not.null );",
|
||||
"",
|
||||
"pm.variables.set(\"id\", pm.response.json().id);"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Transformers\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/spring-boot-rest/foos",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"spring-boot-rest",
|
||||
"foos"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "get a foo",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "03de440c-b483-4ab8-a11a-d0c99b349963",
|
||||
"exec": [
|
||||
"pm.test(\"success status\", () => pm.response.to.be.success );",
|
||||
"",
|
||||
"pm.test(\"name is correct\", () => ",
|
||||
" pm.expect(pm.response.json().name).to.equal(\"Transformers\"));",
|
||||
"",
|
||||
"pm.test(\"id is correct\", () => ",
|
||||
" pm.expect(pm.response.json().id).to.equal(pm.variables.get(\"id\")) );"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": ""
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/spring-boot-rest/foos/{{id}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"spring-boot-rest",
|
||||
"foos",
|
||||
"{{id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "delete a foo",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "74c1bb0f-c06c-48b1-a545-459233541b14",
|
||||
"exec": [
|
||||
"pm.test(\"success status\", () => pm.response.to.be.success );"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": ""
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/spring-boot-rest/foos/{{id}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"spring-boot-rest",
|
||||
"foos",
|
||||
"{{id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "verify delete",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"id": "03de440c-b483-4ab8-a11a-d0c99b349963",
|
||||
"exec": [
|
||||
"pm.test(\"status is 500\", () => pm.response.to.have.status(500) );",
|
||||
"",
|
||||
"pm.test(\"no value present\", () => ",
|
||||
" pm.expect(pm.response.json().cause).to.equal(\"No value present\"));"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": ""
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/spring-boot-rest/foos/{{id}}",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"spring-boot-rest",
|
||||
"foos",
|
||||
"{{id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package com.baeldung.etag;
|
||||
|
||||
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;
|
||||
|
||||
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 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;
|
||||
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package com.baeldung.mime;
|
||||
|
||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.baeldung.etag.Foo;
|
||||
import com.baeldung.etag.WebConfig;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes= WebConfig.class, webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@ComponentScan({"com.baeldung.mime", "com.baeldung.etag"})
|
||||
@EnableAutoConfiguration
|
||||
@ActiveProfiles("test")
|
||||
public class FooLiveTest {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
protected IMarshaller marshaller;
|
||||
|
||||
// API
|
||||
|
||||
public final void create() {
|
||||
create(new Foo(randomAlphabetic(6)));
|
||||
}
|
||||
|
||||
public final String createAsUri() {
|
||||
return createAsUri(new Foo(randomAlphabetic(6)));
|
||||
}
|
||||
|
||||
protected final void create(final Foo resource) {
|
||||
createAsUri(resource);
|
||||
}
|
||||
|
||||
private final String createAsUri(final Foo resource) {
|
||||
final Response response = createAsResponse(resource);
|
||||
return getURL() + "/" + response.getBody().as(Foo.class).getId();
|
||||
}
|
||||
|
||||
private final Response createAsResponse(final Foo resource) {
|
||||
|
||||
final String resourceAsString = marshaller.encode(resource);
|
||||
return RestAssured.given()
|
||||
.contentType(marshaller.getMime())
|
||||
.body(resourceAsString)
|
||||
.post(getURL());
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
protected String getURL() {
|
||||
return "http://localhost:" + port + "/foos";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {
|
||||
// Given
|
||||
final String uriOfResource = createAsUri();
|
||||
|
||||
// When
|
||||
final Response findOneResponse = RestAssured.given().header("Accept", "application/json").get(uriOfResource);
|
||||
|
||||
// Then
|
||||
assertEquals(findOneResponse.getStatusCode(), 200);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.baeldung.mime;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IMarshaller {
|
||||
|
||||
<T> String encode(final T entity);
|
||||
|
||||
<T> T decode(final String entityAsString, final Class<T> clazz);
|
||||
|
||||
<T> List<T> decodeList(final String entitiesAsString, final Class<T> clazz);
|
||||
|
||||
String getMime();
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package com.baeldung.mime;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import com.baeldung.etag.Foo;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public final class JacksonMarshaller implements IMarshaller {
|
||||
private final Logger logger = LoggerFactory.getLogger(JacksonMarshaller.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JacksonMarshaller() {
|
||||
super();
|
||||
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
@Override
|
||||
public final <T> String encode(final T resource) {
|
||||
String entityAsJSON = null;
|
||||
try {
|
||||
entityAsJSON = objectMapper.writeValueAsString(resource);
|
||||
} catch (final IOException ioEx) {
|
||||
logger.error("", ioEx);
|
||||
}
|
||||
|
||||
return entityAsJSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final <T> T decode(final String resourceAsString, final Class<T> clazz) {
|
||||
T entity = null;
|
||||
try {
|
||||
entity = objectMapper.readValue(resourceAsString, clazz);
|
||||
} catch (final IOException ioEx) {
|
||||
logger.error("", ioEx);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public final <T> List<T> decodeList(final String resourcesAsString, final Class<T> clazz) {
|
||||
List<T> entities = null;
|
||||
try {
|
||||
if (clazz.equals(Foo.class)) {
|
||||
entities = objectMapper.readValue(resourcesAsString, new TypeReference<List<T>>() {
|
||||
// ...
|
||||
});
|
||||
} else {
|
||||
entities = objectMapper.readValue(resourcesAsString, List.class);
|
||||
}
|
||||
} catch (final IOException ioEx) {
|
||||
logger.error("", ioEx);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMime() {
|
||||
return MediaType.APPLICATION_JSON.toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package com.baeldung.mime;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Profile("test")
|
||||
public class TestMarshallerFactory implements FactoryBean<IMarshaller> {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
public TestMarshallerFactory() {
|
||||
super();
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
@Override
|
||||
public IMarshaller getObject() {
|
||||
final String testMime = env.getProperty("test.mime");
|
||||
if (testMime != null) {
|
||||
switch (testMime) {
|
||||
case "json":
|
||||
return new JacksonMarshaller();
|
||||
case "xml":
|
||||
return new XStreamMarshaller();
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
return new JacksonMarshaller();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<IMarshaller> getObjectType() {
|
||||
return IMarshaller.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.baeldung.mime;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import com.baeldung.etag.Foo;
|
||||
import com.thoughtworks.xstream.XStream;
|
||||
|
||||
public final class XStreamMarshaller implements IMarshaller {
|
||||
|
||||
private XStream xstream;
|
||||
|
||||
public XStreamMarshaller() {
|
||||
super();
|
||||
|
||||
xstream = new XStream();
|
||||
xstream.autodetectAnnotations(true);
|
||||
xstream.processAnnotations(Foo.class);
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
@Override
|
||||
public final <T> String encode(final T resource) {
|
||||
return xstream.toXML(resource);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public final <T> T decode(final String resourceAsString, final Class<T> clazz) {
|
||||
return (T) xstream.fromXML(resourceAsString);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> List<T> decodeList(final String resourcesAsString, final Class<T> clazz) {
|
||||
return this.decode(resourcesAsString, List.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMime() {
|
||||
return MediaType.APPLICATION_XML.toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.baeldung.students;
|
||||
|
||||
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.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
public class StudentControllerIntegrationTest {
|
||||
|
||||
private static final String STUDENTS_PATH = "/students/";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
public void whenReadAll_thenStatusIsOk() throws Exception {
|
||||
this.mockMvc.perform(get(STUDENTS_PATH))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenReadOne_thenStatusIsOk() throws Exception {
|
||||
this.mockMvc.perform(get(STUDENTS_PATH + 1))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenCreate_thenStatusIsCreated() throws Exception {
|
||||
Student student = new Student(10, "Albert", "Einstein");
|
||||
this.mockMvc.perform(post(STUDENTS_PATH).content(asJsonString(student))
|
||||
.contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUpdate_thenStatusIsOk() throws Exception {
|
||||
Student student = new Student(1, "Nikola", "Tesla");
|
||||
this.mockMvc.perform(put(STUDENTS_PATH + 1)
|
||||
.content(asJsonString(student))
|
||||
.contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenDelete_thenStatusIsNoContent() throws Exception {
|
||||
this.mockMvc.perform(delete(STUDENTS_PATH + 3))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
private String asJsonString(final Object obj) {
|
||||
try {
|
||||
return new ObjectMapper().writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,12 +4,7 @@ This module contains articles about Spring Boot RESTful APIs.
|
|||
|
||||
### Relevant Articles
|
||||
|
||||
- [HATEOAS for a Spring REST Service](https://www.baeldung.com/rest-api-discoverability-with-spring)
|
||||
- [Versioning a REST API](https://www.baeldung.com/rest-versioning)
|
||||
- [ETags for REST with Spring](https://www.baeldung.com/etags-for-rest-with-spring)
|
||||
- [Testing REST with multiple MIME types](https://www.baeldung.com/testing-rest-api-with-multiple-media-types)
|
||||
- [Testing Web APIs with Postman Collections](https://www.baeldung.com/postman-testing-collections)
|
||||
- [Spring Boot Consuming and Producing JSON](https://www.baeldung.com/spring-boot-json)
|
||||
|
||||
### E-book
|
||||
|
||||
|
@ -25,6 +20,7 @@ These articles are part of the Spring REST E-book:
|
|||
8. [An Intro to Spring HATEOAS](https://www.baeldung.com/spring-hateoas-tutorial)
|
||||
9. [REST Pagination in Spring](https://www.baeldung.com/rest-api-pagination-in-spring)
|
||||
10. [Test a REST API with Java](https://www.baeldung.com/integration-testing-a-rest-api)
|
||||
11. [HATEOAS for a Spring REST Service](https://www.baeldung.com/rest-api-discoverability-with-spring)
|
||||
|
||||
NOTE:
|
||||
Since this is a module tied to an e-book, it should not be moved or used to store the code for any further article.
|
||||
|
|
Loading…
Reference in New Issue