Commit f14e88f2 authored by Colin DAMON's avatar Colin DAMON

Internationalized exceptions

parent eead5197
Pipeline #30064 passed with stage
in 6 minutes and 37 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;
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(String errorType, String message) {
this(errorType, message, null);
}
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.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.ippon.pouet.common.infrastructure.primary.PouetFieldError.PouetFieldErrorBuilder;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@JsonDeserialize(builder = PouetFieldErrorBuilder.class)
@ApiModel(description = "Error for a field validation")
class PouetFieldError {
@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 PouetFieldError(PouetFieldErrorBuilder builder) {
fieldPath = builder.fieldPath;
reason = builder.reason;
message = builder.message;
}
public static PouetFieldErrorBuilder builder() {
return new PouetFieldErrorBuilder();
}
public String getFieldPath() {
return fieldPath;
}
public String getReason() {
return reason;
}
public String getMessage() {
return message;
}
@JsonPOJOBuilder(withPrefix = "")
public static class PouetFieldErrorBuilder {
private String fieldPath;
private String reason;
private String message;
public PouetFieldErrorBuilder fieldPath(String fieldPath) {
this.fieldPath = fieldPath;
return this;
}
public PouetFieldErrorBuilder reason(String reason) {
this.reason = reason;
return this;
}
public PouetFieldErrorBuilder message(String message) {
this.message = message;
return this;
}
public PouetFieldError build() {
return new PouetFieldError(this);
}
}
}
package com.ippon.pouet.common.infrastructure.primary;
public final class ValidationMessage {
public static final String MANDATORY = "user.mandatory";
public static final String WRONG_FORMAT = "user.wrong-format";
private ValidationMessage() {}
}
package com.ippon.pouet.config;
import com.ippon.pouet.common.domain.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);
}
}
......@@ -6,8 +6,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.ippon.pouet.common.domain.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
......@@ -34,20 +32,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();
}
}
package com.ippon.pouet.config;
import com.ippon.pouet.common.domain.Generated;
import com.ippon.pouet.security.*;
import com.ippon.pouet.security.jwt.*;
import com.ippon.pouet.security.AuthoritiesConstants;
import com.ippon.pouet.security.jwt.JWTConfigurer;
import com.ippon.pouet.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;
......@@ -17,22 +17,21 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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;
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(TokenProvider tokenProvider, CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
private final AuthenticationErrorsHandler errorsHandler;
public SecurityConfiguration(TokenProvider tokenProvider, CorsFilter corsFilter, AuthenticationErrorsHandler errorsHandler) {
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
this.errorsHandler = errorsHandler;
}
@Bean
......@@ -61,8 +60,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.authenticationEntryPoint(errorsHandler)