BAEL-2414 Guide to problem-spring-web (#6147)

* Fix issue with package name and folder name mismatch

* Add problem-spring-web code samples

* Use the latest version of the problem-spring-web library. Add spring security exceptions handling samples

* Fix issue with security configuration. Fix sample for forbidden operation

* Update ProblemDemoConfiguration.java

* Add integration tests to validate problems are correctly handled and responses are generated using the problem library
This commit is contained in:
Fabian Rivera 2019-01-22 23:54:24 -06:00 committed by maibin
parent 36eac84ff9
commit 7fa2aba180
12 changed files with 409 additions and 130 deletions

View File

@ -19,6 +19,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId> <artifactId>spring-boot-starter-tomcat</artifactId>
@ -29,6 +33,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Problem Spring Web -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>${problem-spring-web.version}</version>
</dependency>
<!-- ShedLock --> <!-- ShedLock -->
<dependency> <dependency>
<groupId>net.javacrumbs.shedlock</groupId> <groupId>net.javacrumbs.shedlock</groupId>
@ -139,6 +150,7 @@
<guava.version>18.0</guava.version> <guava.version>18.0</guava.version>
<git-commit-id-plugin.version>2.2.4</git-commit-id-plugin.version> <git-commit-id-plugin.version>2.2.4</git-commit-id-plugin.version>
<modelmapper.version>2.3.2</modelmapper.version> <modelmapper.version>2.3.2</modelmapper.version>
<problem-spring-web.version>0.23.0</problem-spring-web.version>
</properties> </properties>
</project> </project>

View File

@ -1,4 +1,4 @@
package org.baeldung.boot; package com.baeldung.boot;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@ -0,0 +1,19 @@
package com.baeldung.boot.problem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
@ComponentScan("com.baeldung.boot.problem")
public class SpringProblemApplication {
public static void main(String[] args) {
System.setProperty("spring.profiles.active", "problem");
SpringApplication.run(SpringProblemApplication.class, args);
}
}

View File

@ -0,0 +1,9 @@
package com.baeldung.boot.problem.advice;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.zalando.problem.spring.web.advice.ProblemHandling;
@ControllerAdvice
public class ExceptionHandler implements ProblemHandling {
}

View File

@ -0,0 +1,9 @@
package com.baeldung.boot.problem.advice;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait;
@ControllerAdvice
public class SecurityExceptionHandler implements SecurityAdviceTrait {
}

View File

@ -0,0 +1,17 @@
package com.baeldung.boot.problem.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.problem.ProblemModule;
import org.zalando.problem.validation.ConstraintViolationProblemModule;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public class ProblemDemoConfiguration {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().registerModules(new ProblemModule(), new ConstraintViolationProblemModule());
}
}

View File

@ -0,0 +1,31 @@
package com.baeldung.boot.problem.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;
@Configuration
@EnableWebSecurity
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProblemSupport problemSupport;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/")
.permitAll();
http.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport);
}
}

View File

@ -0,0 +1,56 @@
package com.baeldung.boot.problem.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baeldung.boot.problem.dto.Task;
import com.baeldung.boot.problem.problems.TaskNotFoundProblem;
@RestController
@RequestMapping("/tasks")
public class ProblemDemoController {
private static final Map<Long, Task> MY_TASKS;
static {
MY_TASKS = new HashMap<>();
MY_TASKS.put(1L, new Task(1L, "My first task"));
MY_TASKS.put(2L, new Task(2L, "My second task"));
}
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public List<Task> getTasks() {
return new ArrayList<>(MY_TASKS.values());
}
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Task getTasks(@PathVariable("id") Long taskId) {
if (MY_TASKS.containsKey(taskId)) {
return MY_TASKS.get(taskId);
} else {
throw new TaskNotFoundProblem(taskId);
}
}
@PutMapping("/{id}")
public void updateTask(@PathVariable("id") Long id) {
throw new UnsupportedOperationException();
}
@DeleteMapping("/{id}")
public void deleteTask(@PathVariable("id") Long id) {
throw new AccessDeniedException("You can't delete this task");
}
}

View File

@ -0,0 +1,32 @@
package com.baeldung.boot.problem.dto;
public class Task {
private Long id;
private String description;
public Task() {
}
public Task(Long id, String description) {
this.id = id;
this.description = description;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,16 @@
package com.baeldung.boot.problem.problems;
import java.net.URI;
import org.zalando.problem.AbstractThrowableProblem;
import org.zalando.problem.Status;
public class TaskNotFoundProblem extends AbstractThrowableProblem {
private static final URI TYPE = URI.create("https://example.org/not-found");
public TaskNotFoundProblem(Long taskId) {
super(TYPE, "Not found", Status.NOT_FOUND, String.format("Task '%s' not found", taskId));
}
}

View File

@ -0,0 +1,3 @@
spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
spring.http.encoding.force=true

View File

@ -0,0 +1,75 @@
package com.baeldung.boot.problem.controller;
import static org.hamcrest.CoreMatchers.equalTo;
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.put;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
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.baeldung.boot.problem.SpringProblemApplication;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = SpringProblemApplication.class)
@AutoConfigureMockMvc
public class ProblemDemoControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void whenRequestingAllTasks_thenReturnSuccessfulResponseWithArrayWithTwoTasks() throws Exception {
mockMvc.perform(get("/tasks").contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(jsonPath("$.length()", equalTo(2)))
.andExpect(status().isOk());
}
@Test
public void whenRequestingExistingTask_thenReturnSuccessfulResponse() throws Exception {
mockMvc.perform(get("/tasks/1").contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(jsonPath("$.id", equalTo(1)))
.andExpect(status().isOk());
}
@Test
public void whenRequestingMissingTask_thenReturnNotFoundProblemResponse() throws Exception {
mockMvc.perform(get("/tasks/5").contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE))
.andDo(print())
.andExpect(jsonPath("$.title", equalTo("Not found")))
.andExpect(jsonPath("$.status", equalTo(404)))
.andExpect(jsonPath("$.detail", equalTo("Task '5' not found")))
.andExpect(status().isNotFound());
}
@Test
public void whenMakePutCall_thenReturnNotImplementedProblemResponse() throws Exception {
mockMvc.perform(put("/tasks/1").contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE))
.andDo(print())
.andExpect(jsonPath("$.title", equalTo("Not Implemented")))
.andExpect(jsonPath("$.status", equalTo(501)))
.andExpect(status().isNotImplemented());
}
@Test
public void whenMakeDeleteCall_thenReturnForbiddenProblemResponse() throws Exception {
mockMvc.perform(delete("/tasks/2").contentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE))
.andDo(print())
.andExpect(jsonPath("$.title", equalTo("Forbidden")))
.andExpect(jsonPath("$.status", equalTo(403)))
.andExpect(jsonPath("$.detail", equalTo("You can't delete this task")))
.andExpect(status().isForbidden());
}
}