Commit 7194bb00 authored by Colin DAMON's avatar Colin DAMON

Custom errors management

parent 51b24085
......@@ -245,10 +245,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>
......@@ -305,6 +301,11 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
......
package com.ippon.borestop.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.borestop.common.domain.error;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Parent exception used in Borestop application. Those exceptions will be resolved as human readable errors.
*
* <p>
* You can use this implementation directly:
* </p>
*
* <p>
* <code>
* <pre>
* BorestopException.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 BorestopException {
*
* public MissingMandatoryValueException(BorestopMessage borestopMessage, String fieldName) {
* this(builder(borestopMessage, fieldName, defaultMessage(fieldName)));
* }
*
* protected MissingMandatoryValueException(BorestopExceptionBuilder builder) {
* super(builder);
* }
*
* private static BorestopExceptionBuilder builder(BorestopMessage borestopMessage, String fieldName, String message) {
* return BorestopException.builder(borestopMessage)
* .status(ErrorsHttpStatus.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 BorestopException extends RuntimeException {
private final Map<String, String> arguments;
private final ErrorStatus status;
private final BorestopMessage borestopMessage;
protected BorestopException(BorestopExceptionBuilder builder) {
super(getMessage(builder), getCause(builder));
arguments = getArguments(builder);
status = getStatus(builder);
borestopMessage = getBorestopMessage(builder);
}
private static String getMessage(BorestopExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.message;
}
private static Throwable getCause(BorestopExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.cause;
}
private static Map<String, String> getArguments(BorestopExceptionBuilder builder) {
if (builder == null) {
return null;
}
return Collections.unmodifiableMap(builder.arguments);
}
private static ErrorStatus getStatus(BorestopExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.status;
}
private static BorestopMessage getBorestopMessage(BorestopExceptionBuilder builder) {
if (builder == null) {
return null;
}
return builder.borestopMessage;
}
public static BorestopExceptionBuilder builder(BorestopMessage message) {
return new BorestopExceptionBuilder(message);
}
public Map<String, String> getArguments() {
return arguments;
}
public ErrorStatus getStatus() {
return status;
}
public BorestopMessage getBorestopMessage() {
return borestopMessage;
}
public static class BorestopExceptionBuilder {
private final Map<String, String> arguments = new HashMap<>();
private String message;
private ErrorStatus status;
private BorestopMessage borestopMessage;
private Throwable cause;
public BorestopExceptionBuilder(BorestopMessage borestopMessage) {
this.borestopMessage = borestopMessage;
}
public BorestopExceptionBuilder 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 BorestopExceptionBuilder status(ErrorStatus status) {
this.status = status;
return this;
}
public BorestopExceptionBuilder message(String message) {
this.message = message;
return this;
}
public BorestopExceptionBuilder cause(Throwable cause) {
this.cause = cause;
return this;
}
public BorestopException build() {
return new BorestopException(this);
}
}
}
package com.ippon.borestop.common.domain.error;
import java.io.Serializable;
@FunctionalInterface
public interface BorestopMessage extends Serializable {
String getMessageKey();
}
package com.ippon.borestop.common.domain.error;
public enum ErrorStatus {
BAD_REQUEST,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
CONFLICT,
INTERNAL_SERVER_ERROR
}
package com.ippon.borestop.common.domain.error;
public class MissingMandatoryValueException extends BorestopException {
protected MissingMandatoryValueException(BorestopExceptionBuilder builder) {
super(builder);
}
private static BorestopExceptionBuilder builder(BorestopMessage borestopMessage, String fieldName, String message) {
return BorestopException
.builder(borestopMessage)
.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";
}
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)")
);
}
}
package com.ippon.borestop.common.domain.error;
public enum StandardMessage implements BorestopMessage {
USER_MANDATORY("user.mandatory"),
BAD_REQUEST("user.bad-request"),
INTERNAL_SERVER_ERROR("server.internal-server-error"),
SERVER_MANDATORY_NULL("server.mandatory-null"),
SERVER_MANDATORY_BLANK("server.mandatory-blank");
private final String messageKey;
private StandardMessage(String code) {
this.messageKey = code;
}
@Override
public String getMessageKey() {
return messageKey;
}
}
package com.ippon.borestop.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.borestop.common.infrastructure.primary;
import com.ippon.borestop.common.domain.error.BorestopMessage;
enum AuthenticationMessage implements BorestopMessage {
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.borestop.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")
public class BorestopError {
@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<BorestopFieldError> fieldsErrors;
public BorestopError(
@JsonProperty("errorType") String errorType,
@JsonProperty("message") String message,
@JsonProperty("fieldsErrors") List<BorestopFieldError> fieldsErrors
) {
this.errorType = errorType;
this.message = message;
this.fieldsErrors = fieldsErrors;
}
public String getErrorType() {
return errorType;
}
public String getMessage() {
return message;
}
public Collection<BorestopFieldError> getFieldsErrors() {
return fieldsErrors;
}
}
/**
*
*/
package com.ippon.borestop.common.infrastructure.primary;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.ippon.borestop.common.infrastructure.primary.BorestopFieldError.BorestopFieldErrorBuilder;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@JsonDeserialize(builder = BorestopFieldErrorBuilder.class)
@ApiModel(description = "Error for a field validation")
public class BorestopFieldError {
@ApiModelProperty(value = "Path to the field in error", example = "address.country", required = true)
private final String fieldPath;
@ApiModelProperty(value = "Technical reason for the invalidation", example = "user.mandatory", required = true)
private final String reason;
@ApiModelProperty(value = "Human readable message for the invalidation", example = "Le champ doit être renseigné", required = true)
private final String message;
private BorestopFieldError(BorestopFieldErrorBuilder builder) {
fieldPath = builder.fieldPath;
reason = builder.reason;
message = builder.message;
}
public static BorestopFieldErrorBuilder builder() {
return new BorestopFieldErrorBuilder();
}
public String getFieldPath() {
return fieldPath;
}
public String getReason() {
return reason;
}
public String getMessage() {
return message;
}
@JsonPOJOBuilder(withPrefix = "")
public static class BorestopFieldErrorBuilder {
private String fieldPath;
private String reason;
private String message;
public BorestopFieldErrorBuilder fieldPath(String fieldPath) {
this.fieldPath = fieldPath;
return this;
}
public BorestopFieldErrorBuilder reason(String reason) {
this.reason = reason;
return this;
}
public BorestopFieldErrorBuilder message(String message) {
this.message = message;
return this;
}
public BorestopFieldError build() {
return new BorestopFieldError(this);
}
}
}
package com.ippon.borestop.common.infrastructure.primary;
public final class ValidationMessage {
public static final String MANDATORY = "user.mandatory";
public static final String WRONG_FORMAT = "user.wrong-format";
public static final String RPPS_FORMAT = "user.wrong-national-id-format";
public static final String MAIL_FORMAT = "user.wrong-mail-format";
public static final String VALUE_TOO_LOW = "user.too-low";
public static final String VALUE_TOO_HIGH = "user.too-high";
private ValidationMessage() {}
}
package com.ippon.borestop.config;
import com.ippon.borestop.common.infrastructure.Generated;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
@Generated
@Component
class AuthenticationErrorsHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private final HandlerExceptionResolver resolver;
@Autowired
public AuthenticationErrorsHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception)
throws IOException, ServletException {
resolver.resolveException(request, response, null, exception);
}
@Override
public void handle(final HttpServletRequest request, final HttpServletResponse response, final AccessDeniedException exception)
throws IOException, ServletException {
resolver.resolveException(request, response, null, exception);
}
}
......@@ -11,7 +11,7 @@ public final class Constants {
public static final String LOGIN_REGEX = "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$";
public static final String SYSTEM_ACCOUNT = "system";
public static final String DEFAULT_LANGUAGE = "en";
public static final String DEFAULT_LANGUAGE = "fr";
public static final String ANONYMOUS_USER = "anonymoususer";
private Constants() {}
......
......@@ -6,8 +6,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.ippon.borestop.common.infrastructure.Generated;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.problem.ProblemModule;
import org.zalando.problem.violations.ConstraintViolationProblemModule;
@Generated
@Configuration
......@@ -15,6 +13,7 @@ public class JacksonConfiguration {
/**
* Support for Java date and time API.
*
* @return the corresponding Jackson module.
*/
@Bean
......@@ -34,20 +33,4 @@ public class JacksonConfiguration {
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
/*
* Module for serialization/deserialization of RFC7807 Problem.
*/
@Bean
public ProblemModule problemModule() {
return new ProblemModule();
}
/*
* Module for serialization/deserialization of ConstraintViolationProblem.
*/
@Bean
public ConstraintViolationProblemModule constraintViolationProblemModule() {
return new ConstraintViolationProblemModule();
}
}
......@@ -5,7 +5,6 @@ import com.ippon.borestop.security.AuthoritiesConstants;
import com.ippon.borestop.security.jwt.JWTConfigurer;
import com.ippon.borestop.security.jwt.TokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
......@@ -22,22 +21,20 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.web.filter.CorsFilter;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;
@Generated
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;