Commit 2197b8a0 authored by Colin DAMON's avatar Colin DAMON
Browse files

Internationalized exceptions

parent eead5197
Pipeline #30063 passed with stage
in 8 minutes and 34 seconds
......@@ -244,10 +244,6 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
......@@ -279,6 +275,11 @@
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<scope>test</scope>
</dependency>
<!-- jhipster-needle-maven-add-dependency -->
</dependencies>
......@@ -498,7 +499,7 @@
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<configuration>
<excludes>
<excludes>
<exclude>com/ippon/pouet/domain/*</exclude>
</excludes>
</configuration>
......
package com.ippon.pouet.common.domain.error;
public final class Assert {
private Assert() {}
public static void notNull(String field, Object input) {
if (input == null) {
throw MissingMandatoryValueException.forNullValue(field);
}
}
public static void notBlank(String field, String input) {
notNull(field, input);
if (input.isBlank()) {
throw MissingMandatoryValueException.forBlankValue(field);
}
}
}
package com.ippon.pouet.common.domain.error;
public enum ErrorStatus {
BAD_REQUEST,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
INTERNAL_SERVER_ERROR,
}
package com.ippon.pouet.common.domain.error;
public class MissingMandatoryValueException extends PouetException {
protected MissingMandatoryValueException(PouetExceptionBuilder builder) {
super(builder);
}
public static MissingMandatoryValueException forNullValue(String fieldName) {
return new MissingMandatoryValueException(
builder(StandardMessage.SERVER_MANDATORY_NULL, fieldName, defaultMessage(fieldName) + " (null)")
);
}
public static MissingMandatoryValueException forBlankValue(String fieldName) {
return new MissingMandatoryValueException(
builder(StandardMessage.SERVER_MANDATORY_BLANK, fieldName, defaultMessage(fieldName) + " (blank)")
);
}
private static PouetExceptionBuilder builder(PouetMessage pouetMessage, String fieldName, String message) {
return PouetException.builder(pouetMessage).status(ErrorStatus.INTERNAL_SERVER_ERROR).argument("field", fieldName).message(message);
}
private static String defaultMessage(String fieldName) {
return "The field \"" + fieldName + "\" is mandatory and wasn't set";
}
}
package com.ippon.pouet.common.domain.error;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Parent exception used in Pouet application. Those exceptions will be resolved as human readable errors.
*
* <p>
* You can use this implementation directly:
* </p>
*
* <p>
* <code>
* <pre>
* PouetException.builder(StandardMessages.USER_MANDATORY)
* .argument("key", "value")
* .argument("other", "test")
* .status(ErrorsHttpStatus.BAD_REQUEST)
* .message("Error message")
* .cause(new RuntimeException())
* .build();
* </pre>
* </code>
* </p>
*
* <p>
* Or make extension exceptions:
* </p>
*
* <p>
* <code>
* <pre>
* public class MissingMandatoryValueException extends PouetException {
*
* public MissingMandatoryValueException(PouetMessage pouetMessage, String fieldName) {
* this(builder(pouetMessage, fieldName, defaultMessage(fieldName)));
* }
*
* protected MissingMandatoryValueException(PouetExceptionBuilder builder) {
* super(builder);
* }
*
* private static PouetExceptionBuilder builder(PouetMessage pouetMessage, String fieldName, String message) {
* return PouetException.builder(pouetMessage)
* .status(ErrorsStatus.INTERNAL_SERVER_ERROR)
* .argument("field", fieldName)
* .message(message);
* }
*
* private static String defaultMessage(String fieldName) {
* return "The field \"" + fieldName + "\" is mandatory and wasn't set";
* }
* }
* </pre>
* </code>
* </p>
*/
public class PouetException extends RuntimeException {
private final Map<String, String> arguments;
private final ErrorStatus status;
private final PouetMessage pouetMessage;
protected PouetException(PouetExceptionBuilder builder) {
super(getMessage(builder), getCause(builder));
arguments = getArguments(builder);
status = getStatus(builder);
pouetMessage = getPouetMessage(builder);
}
private static String getMessage(PouetExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.message;
}
private static Throwable getCause(PouetExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.cause;
}
private static Map<String, String> getArguments(PouetExceptionBuilder builder) {
if (builder == null) {
return null;
}
return Collections.unmodifiableMap(builder.arguments);
}
private static ErrorStatus getStatus(PouetExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.status;
}
private static PouetMessage getPouetMessage(PouetExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.pouetMessage;
}
public static PouetExceptionBuilder builder(PouetMessage message) {
return new PouetExceptionBuilder(message);
}
public Map<String, String> getArguments() {
return arguments;
}
public ErrorStatus getStatus() {
return status;
}
public PouetMessage getPouetMessage() {
return pouetMessage;
}
public static class PouetExceptionBuilder {
private final Map<String, String> arguments = new HashMap<>();
private String message;
private ErrorStatus status;
private PouetMessage pouetMessage;
private Throwable cause;
public PouetExceptionBuilder(PouetMessage pouetMessage) {
this.pouetMessage = pouetMessage;
}
public PouetExceptionBuilder argument(String key, Object value) {
arguments.put(key, getStringValue(value));
return this;
}
private String getStringValue(Object value) {
if (value == null) {
return "null";
}
return value.toString();
}
public PouetExceptionBuilder status(ErrorStatus status) {
this.status = status;
return this;
}
public PouetExceptionBuilder message(String message) {
this.message = message;
return this;
}
public PouetExceptionBuilder cause(Throwable cause) {
this.cause = cause;
return this;
}
public PouetException build() {
return new PouetException(this);
}
}
}
package com.ippon.pouet.common.domain.error;
import java.io.Serializable;
@FunctionalInterface
public interface PouetMessage extends Serializable {
String getMessageKey();
}
package com.ippon.pouet.common.domain.error;
/**
* User messages for standard (common) use cases
*/
public enum StandardMessage implements PouetMessage {
USER_MANDATORY("user.mandatory"),
SERVER_MANDATORY_NULL("server.mandatory-null"),
SERVER_MANDATORY_BLANK("server.mandatory-blank"),
BAD_REQUEST("user.bad-request"),
INTERNAL_SERVER_ERROR("server.internal-server-error");
private final String messageKey;
private StandardMessage(String code) {
this.messageKey = code;
}
@Override
public String getMessageKey() {
return messageKey;
}
}
package com.ippon.pouet.common.infrastructure.primary;
import java.util.Map;
final class ArgumentsReplacer {
private static final String OPEN_MUSTACHE = "\\{\\{\\s*";
private static final String CLOSE_MUSTACHE = "\\s*\\}\\}";
private ArgumentsReplacer() {}
public static String replaceArguments(String message, Map<String, String> arguments) {
if (message == null || arguments == null) {
return message;
}
String result = message;
for (Map.Entry<String, String> argument : arguments.entrySet()) {
String argumentRegex = OPEN_MUSTACHE + argument.getKey() + CLOSE_MUSTACHE;
result = result.replaceAll(argumentRegex, argument.getValue());
}
return result;
}
}
package com.ippon.pouet.common.infrastructure.primary;
import com.ippon.pouet.common.domain.error.PouetMessage;
/**
* Messages for Authentication
*/
enum AuthenticationMessage implements PouetMessage {
NOT_AUTHENTICATED("user.authentication-not-authenticated");
private final String messageKey;
AuthenticationMessage(String messageKey) {
this.messageKey = messageKey;
}
@Override
public String getMessageKey() {
return messageKey;
}
}
/**
*
*/
package com.ippon.pouet.common.infrastructure.primary;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.Collection;
import java.util.List;
@ApiModel(description = "Error result for a WebService call")
class PouetError {
@ApiModelProperty(value = "Technical type of this error", example = "user.mandatory", required = true)
private final String errorType;
@ApiModelProperty(
value = "Human readable error message",
example = "Une erreur technique est survenue lors du traitement de votre demande",
required = true
)
private final String message;
@ApiModelProperty(value = "Invalid fields", required = false)
private final List<PouetFieldError> fieldsErrors;
public PouetError(
@JsonProperty("errorType") String errorType,
@JsonProperty("message") String message,
@JsonProperty("fieldsErrors") List<PouetFieldError> fieldsErrors
) {
this.errorType = errorType;
this.message = message;
this.fieldsErrors = fieldsErrors;
}
public String getErrorType() {
return errorType;
}
public String getMessage() {
return message;
}
public Collection<PouetFieldError> getFieldsErrors() {
return fieldsErrors;
}
}
package com.ippon.pouet.common.infrastructure.primary;
import com.ippon.pouet.common.domain.error.ErrorStatus;
import com.ippon.pouet.common.domain.error.PouetException;
import com.ippon.pouet.common.domain.error.PouetMessage;
import com.ippon.pouet.common.domain.error.StandardMessage;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class PouetErrorHandler extends ResponseEntityExceptionHandler {
private static final String MESSAGE_PREFIX = "pouet.error.";
private static final String DEFAULT_KEY = StandardMessage.INTERNAL_SERVER_ERROR.getMessageKey();
private static final String BAD_REQUEST_KEY = StandardMessage.BAD_REQUEST.getMessageKey();
private static final String STATUS_EXCEPTION_KEY = "status-exception";
private static final Logger logger = LoggerFactory.getLogger(PouetErrorHandler.class);
private final MessageSource messages;
public PouetErrorHandler(MessageSource messages) {
Locale.setDefault(Locale.FRANCE);
this.messages = messages;
}
@ExceptionHandler
public ResponseEntity<PouetError> handlePouetException(PouetException exception) {
HttpStatus status = getStatus(exception);
logError(exception, status);
String messageKey = getMessageKey(status, exception);
PouetError error = new PouetError(messageKey, getMessage(messageKey, exception.getArguments()), null);
return new ResponseEntity<>(error, status);
}
@ExceptionHandler
public ResponseEntity<PouetError> handleResponseStatusException(ResponseStatusException exception) {
HttpStatus status = exception.getStatus();
logError(exception, status);
PouetError error = new PouetError(STATUS_EXCEPTION_KEY, buildErrorStatusMessage(exception), null);
return new ResponseEntity<>(error, status);
}
private String buildErrorStatusMessage(ResponseStatusException exception) {
String reason = exception.getReason();
if (StringUtils.isBlank(reason)) {
Map<String, String> statusArgument = Map.of("status", String.valueOf(exception.getStatus().value()));
return getMessage(STATUS_EXCEPTION_KEY, statusArgument);
}
return reason;
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<PouetError> handleFileSizeException(MaxUploadSizeExceededException maxUploadSizeExceededException) {
logger.warn("File size limit exceeded: {}", maxUploadSizeExceededException.getMessage(), maxUploadSizeExceededException);
PouetError error = new PouetError("server.upload-too-big", getMessage("server.upload-too-big", null), null);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<PouetError> handleAccessDeniedException(AccessDeniedException accessDeniedException) {
PouetError error = new PouetError("user.access-denied", getMessage("user.access-denied", null), null);
return new ResponseEntity<>(error, HttpStatus.FORBIDDEN);
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<PouetError> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
Throwable rootCause = ExceptionUtils.getRootCause(exception);
if (rootCause instanceof PouetException) {
return handlePouetException((PouetException) rootCause);
}
PouetError error = new PouetError(BAD_REQUEST_KEY, getMessage(BAD_REQUEST_KEY, null), null);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
private HttpStatus getStatus(PouetException exception) {
ErrorStatus status = exception.getStatus();
if (status == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
switch (status) {
case BAD_REQUEST:
return HttpStatus.BAD_REQUEST;
case UNAUTHORIZED:
return HttpStatus.UNAUTHORIZED;
case FORBIDDEN:
return HttpStatus.FORBIDDEN;
case NOT_FOUND:
return HttpStatus.NOT_FOUND;
default:
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
private void logError(Exception exception, HttpStatus status) {
if (status.is5xxServerError()) {
logger.error("A server error was sent to a user: {}", exception.getMessage(), exception);
logErrorBody(exception);
} else {
logger.warn("An error was sent to a user: {}", exception.getMessage(), exception);
}
}
private void logErrorBody(Exception exception) {
Throwable cause = exception.getCause();
if (cause instanceof RestClientResponseException) {
RestClientResponseException restCause = (RestClientResponseException) cause;
logger.error("Cause body: {}", restCause.getResponseBodyAsString());
}
}
private String getMessageKey(HttpStatus status, PouetException exception) {
PouetMessage message = exception.getPouetMessage();
if (message == null) {
return getDefaultMessage(status);
}
return message.getMessageKey();
}
private String getDefaultMessage(HttpStatus status) {
if (status.is5xxServerError()) {
return DEFAULT_KEY;
}
return BAD_REQUEST_KEY;
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException exception,
HttpHeaders headers,
HttpStatus status,
WebRequest request
) {
logger.debug("Bean validation error {}", exception.getMessage(), exception);
PouetError error = new PouetError(BAD_REQUEST_KEY, getMessage(BAD_REQUEST_KEY, null), getFieldsError(exception));
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);