Skip to content

Commit

Permalink
Add ProblemHandling
Browse files Browse the repository at this point in the history
• using org.zalando:problem-spring-web
• changes were made according to https://www.baeldung.com/problem-spring-web
  • Loading branch information
daforster committed Mar 27, 2024
1 parent 35213a7 commit ce834c5
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 36 deletions.
5 changes: 5 additions & 0 deletions metasvc-server/webapp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web-starter</artifactId>
<version>0.27.0</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;

@SpringBootApplication
@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)
/*
@SpringBootApplication is a convenience annotation that adds all of the following:
- @Configuration tags the class as a source of bean definitions for the application context.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.zalando.problem.jackson.ProblemModule;
import org.zalando.problem.violations.ConstraintViolationProblemModule;

@Configuration
public class SpringConfigWeb implements WebMvcConfigurer {

@Bean
@Primary
public ObjectMapper objectMapper() {
return new DigitalCollectionsObjectMapper();
return new DigitalCollectionsObjectMapper()
.registerModules(new ProblemModule(), new ConstraintViolationProblemModule());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,98 @@
package io.github.dbmdz.metadata.server.controller.advice;

import de.digitalcollections.model.exception.Problem;
import de.digitalcollections.model.validation.ValidationException;
import io.github.dbmdz.metadata.server.business.api.service.exceptions.ConflictException;
import io.github.dbmdz.metadata.server.business.api.service.exceptions.ResourceNotFoundException;
import java.net.URI;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.util.UriComponentsBuilder;
import org.zalando.problem.Problem;
import org.zalando.problem.Status;
import org.zalando.problem.ThrowableProblem;
import org.zalando.problem.spring.web.advice.ProblemHandling;

@ControllerAdvice
public class ExceptionAdvice {
public class ExceptionAdvice implements ProblemHandling {

private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionAdvice.class);

@ExceptionHandler(Exception.class)
public ResponseEntity<Problem> handleAllOther(Exception exception) {
LOGGER.error("exception stack trace", exception);
Throwable cause = exception;
while (cause.getCause() != null) {
cause = cause.getCause();
}
Problem problem =
new Problem(
cause.getClass().getSimpleName(), "Service Exception", 500, cause.getMessage(), null);
return new ResponseEntity<>(problem, HttpStatus.INTERNAL_SERVER_ERROR);
private static URI getRequestUri(ServletWebRequest servletRequest) {
HttpServletRequest req = servletRequest.getRequest();
return UriComponentsBuilder.fromPath(req.getRequestURI())
.query(req.getQueryString())
.build()
.toUri();
}

@ResponseStatus(HttpStatus.CONFLICT)
@ExceptionHandler(ConflictException.class)
public void handleConflict(ConflictException exception) {}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public void handleHttpMediaTypeNotAcceptableException() {}
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<Problem> handleNotFound(
UsernameNotFoundException e, ServletWebRequest request) {
return create(Status.NOT_FOUND, e, request);
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(value = {ResourceNotFoundException.class, UsernameNotFoundException.class})
public void handleNotFound() {}
private static Status statusFromExceptionClass(Throwable exc) {
if (exc instanceof ResourceNotFoundException) {
return Status.NOT_FOUND;
} else if (exc instanceof ConflictException) {
return Status.CONFLICT;
} else if (exc instanceof ValidationException) {
return Status.UNPROCESSABLE_ENTITY;
} else if (exc instanceof HttpMediaTypeNotAcceptableException) {
return Status.NOT_FOUND;
} else {
return Status.INTERNAL_SERVER_ERROR;
}
}

@ExceptionHandler(ValidationException.class)
public ResponseEntity<Object> handleValidationException(ValidationException exception) {
Map<String, Object> body = new LinkedHashMap<>(3);
body.put("timestamp", new Date());
body.put("errors", exception.getErrors());
if (StringUtils.hasText(exception.getMessage())) body.put("message", exception.getMessage());
public ResponseEntity<Problem> handleValidationException(
ValidationException exception, ServletWebRequest request) {
ThrowableProblem problem =
Problem.builder()
.withType(
UriComponentsBuilder.fromPath("/errors/")
.path(exception.getClass().getSimpleName())
.build()
.toUri())
.withTitle("Validation Exception")
.withStatus(statusFromExceptionClass(exception))
.withInstance(getRequestUri(request))
.withDetail(exception.getMessage())
.with("errors", exception.getErrors())
.with("timestamp", new Date())
.build();
return create(problem, request);
}

return new ResponseEntity<>(body, HttpStatus.UNPROCESSABLE_ENTITY);
@ExceptionHandler
public ResponseEntity<Problem> handleAllOther(Exception exception, ServletWebRequest request) {
Throwable cause = exception;
while (cause.getCause() != null) {
cause = cause.getCause();
}
ThrowableProblem problem =
Problem.builder()
.withType(
UriComponentsBuilder.fromPath("/errors/")
.path(cause.getClass().getSimpleName())
.build()
.toUri())
.withTitle("Metadata-service Exception")
.withStatus(statusFromExceptionClass(cause))
.withDetail(cause.getMessage())
.withInstance(getRequestUri(request))
.build();
if (problem.getStatus() == Status.INTERNAL_SERVER_ERROR)
LOGGER.error("Exception stack trace", exception);
return create(problem, request);
}
}
8 changes: 8 additions & 0 deletions metasvc-server/webapp/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ media:
server:
port: 9000
server-header: "@project.name@ [email protected]@"
servlet:
encoding:
force: true

spring:
datasource:
Expand Down Expand Up @@ -90,6 +93,11 @@ spring:
thymeleaf:
cache: false
mode: HTML
web:
resources:
add-mappings: false
mvc:
throw-exception-if-no-handler-found: true

springdoc:
swagger-ui:
Expand Down

0 comments on commit ce834c5

Please sign in to comment.