Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
twitch
live-coding-fr
Commits
7194bb00
Commit
7194bb00
authored
Aug 23, 2020
by
Colin DAMON
Browse files
Custom errors management
parent
51b24085
Changes
60
Hide whitespace changes
Inline
Side-by-side
borestop/pom.xml
View file @
7194bb00
...
...
@@ -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>
...
...
borestop/src/main/java/com/ippon/borestop/common/domain/error/Assert.java
0 → 100644
View file @
7194bb00
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
);
}
}
}
borestop/src/main/java/com/ippon/borestop/common/domain/error/BorestopException.java
0 → 100644
View file @
7194bb00
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
);
}
}
}
borestop/src/main/java/com/ippon/borestop/common/domain/error/BorestopMessage.java
0 → 100644
View file @
7194bb00
package
com.ippon.borestop.common.domain.error
;
import
java.io.Serializable
;
@FunctionalInterface
public
interface
BorestopMessage
extends
Serializable
{
String
getMessageKey
();
}
borestop/src/main/java/com/ippon/borestop/common/domain/error/ErrorStatus.java
0 → 100644
View file @
7194bb00
package
com.ippon.borestop.common.domain.error
;
public
enum
ErrorStatus
{
BAD_REQUEST
,
UNAUTHORIZED
,
FORBIDDEN
,
NOT_FOUND
,
CONFLICT
,
INTERNAL_SERVER_ERROR
}
borestop/src/main/java/com/ippon/borestop/common/domain/error/MissingMandatoryValueException.java
0 → 100644
View file @
7194bb00
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)"
)
);
}
}
borestop/src/main/java/com/ippon/borestop/common/domain/error/StandardMessage.java
0 → 100644
View file @
7194bb00
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
;
}
}
borestop/src/main/java/com/ippon/borestop/common/infrastructure/primary/ArgumentsReplacer.java
0 → 100644
View file @
7194bb00
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
;
}
}
borestop/src/main/java/com/ippon/borestop/common/infrastructure/primary/AuthenticationMessage.java
0 → 100644
View file @
7194bb00
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
;
}
}
borestop/src/main/java/com/ippon/borestop/common/infrastructure/primary/BorestopError.java
0 → 100644
View file @
7194bb00
/**
*
*/
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
;
}
}
borestop/src/main/java/com/ippon/borestop/common/infrastructure/primary/BorestopErrorHandler.java
0 → 100644
View file @
7194bb00
/**
*
*/
package
com.ippon.borestop.common.infrastructure.primary
;
import
com.ippon.borestop.common.domain.error.BorestopException
;
import
com.ippon.borestop.common.domain.error.ErrorStatus
;
import
com.ippon.borestop.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.BindException
;
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
BorestopErrorHandler
extends
ResponseEntityExceptionHandler
{
private
static
final
String
MESSAGE_PREFIX
=
"borestop.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
(
BorestopErrorHandler
.
class
);
private
final
MessageSource
messages
;
public
BorestopErrorHandler
(
MessageSource
messages
)
{
Locale
.
setDefault
(
Locale
.
FRANCE
);
this
.
messages
=
messages
;
}
@ExceptionHandler
public
ResponseEntity
<
BorestopError
>
handleBorestopException
(
BorestopException
exception
)
{
HttpStatus
status
=
getStatus
(
exception
);
logError
(
exception
,
status
);
String
messageKey
=
getMessageKey
(
status
,
exception
);
BorestopError
error
=
new
BorestopError
(
messageKey
,
getMessage
(
messageKey
,
exception
.
getArguments
()),
null
);
return
new
ResponseEntity
<>(
error
,
status
);
}
@ExceptionHandler
public
ResponseEntity
<
BorestopError
>
handleResponseStatusException
(
ResponseStatusException
exception
)
{
HttpStatus
status
=
exception
.
getStatus
();
logError
(
exception
,
status
);
BorestopError
error
=
new
BorestopError
(
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
<
BorestopError
>
handleFileSizeException
(
MaxUploadSizeExceededException
maxUploadSizeExceededException
)
{
logger
.
warn
(
"File size limit exceeded: {}"
,
maxUploadSizeExceededException
.
getMessage
(),
maxUploadSizeExceededException
);
BorestopError
error
=
new
BorestopError
(
"server.upload-too-big"
,
getMessage
(
"server.upload-too-big"
,
null
),
null
);
return
new
ResponseEntity
<>(
error
,
HttpStatus
.
BAD_REQUEST
);
}
@ExceptionHandler
(
AccessDeniedException
.
class
)
public
ResponseEntity
<
BorestopError
>
handleAccessDeniedException
(
AccessDeniedException
accessDeniedException
)
{
BorestopError
error
=
new
BorestopError
(
"user.access-denied"
,
getMessage
(
"user.access-denied"
,
null
),
null
);
return
new
ResponseEntity
<>(
error
,
HttpStatus
.
FORBIDDEN
);
}
@ExceptionHandler
(
MethodArgumentTypeMismatchException
.
class
)
public
ResponseEntity
<
BorestopError
>
handleMethodArgumentTypeMismatchException
(
MethodArgumentTypeMismatchException
exception
)
{
Throwable
rootCause
=
ExceptionUtils
.
getRootCause
(
exception
);
if
(
rootCause
instanceof
BorestopException
)
{
return
handleBorestopException
((
BorestopException
)
rootCause
);
}
BorestopError
error
=
new
BorestopError
(
BAD_REQUEST_KEY
,
getMessage
(
BAD_REQUEST_KEY
,
null
),
null
);
return
new
ResponseEntity
<>(
error
,
HttpStatus
.
BAD_REQUEST
);
}
private
HttpStatus
getStatus
(
BorestopException
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
;
case
CONFLICT:
return
HttpStatus
.
CONFLICT
;
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
,
BorestopException
exception
)
{
if
(
exception
.
getBorestopMessage
()
==
null
)
{
return
getDefaultMessage
(
status
);
}
return
exception
.
getBorestopMessage
().
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
);