diff --git a/src/main/java/me/alidg/errors/ErrorWithArguments.java b/src/main/java/me/alidg/errors/ErrorWithArguments.java
new file mode 100644
index 0000000..ba80d0c
--- /dev/null
+++ b/src/main/java/me/alidg/errors/ErrorWithArguments.java
@@ -0,0 +1,98 @@
+package me.alidg.errors;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * This object represents an error code with to-be-exposed arguments.
+ *
+ * For example, suppose
+ * we have a bean like:
+ *
+ * public class User {
+ *
+ * @Size(min=1, max=7, message="interests.range_limit")
+ * private List<String> interests;
+ * // omitted for the sake of brevity
+ * }
+ *
+ * If the given interest list wasn't valid, then this object would contain
+ * {@code interests.range_limit} as the errorCode and {@code List(Argument(min, 1), Argument(max, 7))}
+ * as the arguments. Later on we can use those exposed values in our message, for example,
+ * the following error template:
+ *
+ * You should define between {0} and {1} interests.
+ *
+ * Would be translated to:
+ *
+ * You should define between 1 and 7 interests.
+ *
+ */
+public class ErrorWithArguments {
+ private final String errorCode;
+ private final List arguments;
+
+ /**
+ * Constructor
+ *
+ * @param errorCode the error code
+ * @param arguments the arguments to use when interpolating the message of the error code
+ */
+ public ErrorWithArguments(String errorCode, List arguments) {
+ this.errorCode = errorCode;
+ this.arguments = arguments == null ? Collections.emptyList() : arguments;
+ }
+
+ /**
+ * Factory method when an error code has no arguments.
+ *
+ * @param errorCode the error code
+ * @return a new {@link ErrorWithArguments} instance
+ */
+ public static ErrorWithArguments noArgumentError(String errorCode) {
+ requireNonNull(errorCode, "The single error code can't be null");
+ return new ErrorWithArguments(errorCode, emptyList());
+ }
+
+ /**
+ * The error code that will be looked up in the messages.
+ *
+ * @return the error code
+ */
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ /**
+ * The arguments to use when interpolating the message of the error code.
+ *
+ * @return a List of {@link Argument} objects
+ */
+ public List getArguments() {
+ return arguments;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ErrorWithArguments that = (ErrorWithArguments) o;
+ return errorCode.equals(that.errorCode) &&
+ arguments.equals(that.arguments);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(errorCode, arguments);
+ }
+
+
+}
diff --git a/src/main/java/me/alidg/errors/HandledException.java b/src/main/java/me/alidg/errors/HandledException.java
index d86ab21..165e802 100644
--- a/src/main/java/me/alidg/errors/HandledException.java
+++ b/src/main/java/me/alidg/errors/HandledException.java
@@ -4,20 +4,18 @@
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
import static java.util.Collections.singleton;
+import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
/**
* Encapsulates details about a handled exception, including:
*
- *
The mapped business level error codes
+ *
The mapped business level error codes and their arguments that can be used for message translation
*
The corresponding HTTP status code
- *
A collection of arguments that can be used for message translation
*
*
* @author Ali Dehghani
@@ -29,7 +27,7 @@ public class HandledException {
* Collection of error codes corresponding to the handled exception. Usually this collection
* contains only one error code but not always, say for validation errors.
*/
- private final Set errorCodes;
+ private final List errors;
/**
* Corresponding status code for the handled exception.
@@ -37,30 +35,34 @@ public class HandledException {
private final HttpStatus statusCode;
/**
- * Collection of to-be-exposed arguments grouped by the error code. This is a mapping
- * between the error code and all its to-be-exposed arguments. For example, suppose
- * we have a bean like:
- *
- * public class User {
+ * Initialize a handled exception with a set of error codes, a HTTP status code and an
+ * optional collection of arguments.
*
- * @Size(min=1, max=7, message="interests.range_limit")
- * private List<String> interests;
- * // omitted for the sake of brevity
- * }
- *
- * If the given interest list wasn't valid, then this map would contain an entry with the
- * {@code interests.range_limit} as the key and {@code List(Argument(min, 1), Argument(max, 7))}
- * as the values. Later on we can use those exposed values in our message, for example,
- * the following error template:
- *
- * You should define between {0} and {1} interests.
- *
- * Would be translated to:
- *
- * You should define between 1 and 7 interests.
- *
+ * @param errors The corresponding error codes for the handled exception.
+ * @param statusCode The corresponding status code for the handled exception.
+ * @throws NullPointerException When one of the required parameters is null.
+ * @throws IllegalArgumentException At least one error code should be provided.
*/
- private final Map> arguments;
+ public HandledException(@NonNull List errors,
+ @NonNull HttpStatus statusCode) {
+ enforcePreconditions(errors, statusCode);
+ this.errors = errors;
+ this.statusCode = statusCode;
+ }
+
+ /**
+ * Initialize a handled exception with an error code, a HTTP status code and an
+ * optional collection of arguments.
+ *
+ * @param error The corresponding error code for the handled exception.
+ * @param statusCode The corresponding status code for the handled exception.
+ * @throws NullPointerException When one of the required parameters is null.
+ * @throws IllegalArgumentException At least one error code should be provided.
+ */
+ public HandledException(@NonNull ErrorWithArguments error,
+ @NonNull HttpStatus statusCode) {
+ this(singletonList(error), statusCode);
+ }
/**
* Initialize a handled exception with a set of error codes, a HTTP status code and an
@@ -71,14 +73,14 @@ public class HandledException {
* @param arguments Arguments to be exposed from the handled exception to the outside world.
* @throws NullPointerException When one of the required parameters is null.
* @throws IllegalArgumentException At least one error code should be provided.
+ * @deprecated This constructor should no longer be used as it does not allow to support the same error code
+ * multiple times
*/
+ @Deprecated
public HandledException(@NonNull Set errorCodes,
@NonNull HttpStatus statusCode,
@Nullable Map> arguments) {
- enforcePreconditions(errorCodes, statusCode);
- this.errorCodes = errorCodes;
- this.statusCode = statusCode;
- this.arguments = arguments == null ? Collections.emptyMap() : arguments;
+ this(convertToErrors(errorCodes, arguments), statusCode);
}
/**
@@ -90,7 +92,10 @@ public HandledException(@NonNull Set errorCodes,
* @param arguments Arguments to be exposed from the handled exception to the outside world.
* @throws NullPointerException When one of the required parameters is null.
* @throws IllegalArgumentException At least one error code should be provided.
+ * @deprecated This constructor should no longer be used as it does not allow to support the same error code
+ * multiple times
*/
+ @Deprecated
public HandledException(@NonNull String errorCode,
@NonNull HttpStatus statusCode,
@Nullable Map> arguments) {
@@ -98,40 +103,71 @@ public HandledException(@NonNull String errorCode,
}
/**
- * @return Collection of mapped error codes.
- * @see #errorCodes
+ *
+ * @return Collection of errors
*/
@NonNull
- public Set getErrorCodes() {
- return errorCodes;
+ public List getErrors() {
+ return errors;
}
/**
- * @return The mapped status code.
- * @see #statusCode
+ * @return Collection of mapped error codes.
+ * @deprecated This method should no longer be used as it does not allow to support the same error code
+ * multiple times
*/
@NonNull
- public HttpStatus getStatusCode() {
- return statusCode;
+ @Deprecated
+ public Set getErrorCodes() {
+ return errors.stream()
+ .map(ErrorWithArguments::getErrorCode)
+ .collect(Collectors.toSet());
}
/**
+ *
* @return Collection of to-be-exposed arguments.
- * @see #arguments
+ * @deprecated This method should no longer be used as it does not allow to support the same error code
+ * multiple times
*/
@NonNull
+ @Deprecated
public Map> getArguments() {
- return arguments;
+ return errors.stream()
+ .collect(Collectors.toMap(ErrorWithArguments::getErrorCode,
+ ErrorWithArguments::getArguments));
}
- private void enforcePreconditions(Set errorCodes, HttpStatus statusCode) {
+ /**
+ * @return The mapped status code.
+ * @see #statusCode
+ */
+ @NonNull
+ public HttpStatus getStatusCode() {
+ return statusCode;
+ }
+
+ private void enforcePreconditions(List errorCodes, HttpStatus statusCode) {
requireNonNull(errorCodes, "Error codes is required");
requireNonNull(statusCode, "Status code is required");
- if (errorCodes.isEmpty())
+ if (errorCodes.isEmpty()) {
throw new IllegalArgumentException("At least one error code should be provided");
+ }
- if (errorCodes.size() == 1 && errorCodes.contains(null))
+ if (errorCodes.size() == 1 && errorCodes.contains(null)) {
throw new NullPointerException("The single error code can't be null");
+ }
}
+
+ @NonNull
+ private static List convertToErrors(Set errorCodes, Map> arguments) {
+ List result = new ArrayList<>();
+ for (String errorCode : errorCodes) {
+ result.add(new ErrorWithArguments(errorCode, arguments.get(errorCode)));
+ }
+
+ return result;
+ }
+
}
diff --git a/src/main/java/me/alidg/errors/WebErrorHandlers.java b/src/main/java/me/alidg/errors/WebErrorHandlers.java
index 14403b3..550bcb4 100644
--- a/src/main/java/me/alidg/errors/WebErrorHandlers.java
+++ b/src/main/java/me/alidg/errors/WebErrorHandlers.java
@@ -233,9 +233,9 @@ private Throwable refineIfNeeded(Throwable exception) {
private List translateErrors(HandledException handled, Locale locale) {
return handled
- .getErrorCodes()
+ .getErrors()
.stream()
- .map(code -> withMessage(code, getArgumentsFor(handled, code), locale))
+ .map(errorWithArguments -> withMessage(errorWithArguments.getErrorCode(), errorWithArguments.getArguments(), locale))
.collect(toList());
}
@@ -265,7 +265,9 @@ private String className(Object toInspect) {
return toInspect.getClass().getName();
}
+/*
private List getArgumentsFor(HandledException handled, String errorCode) {
return handled.getArguments().getOrDefault(errorCode, emptyList());
}
+*/
}
diff --git a/src/main/java/me/alidg/errors/handlers/AnnotatedWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/AnnotatedWebErrorHandler.java
index e777768..c2e64c6 100644
--- a/src/main/java/me/alidg/errors/handlers/AnnotatedWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/AnnotatedWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import me.alidg.errors.annotation.ExceptionMapping;
@@ -18,7 +19,6 @@
import java.util.Objects;
import java.util.stream.Stream;
-import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.toList;
import static me.alidg.errors.Argument.arg;
@@ -72,7 +72,7 @@ public HandledException handle(Throwable exception) {
HttpStatus httpStatus = exceptionMapping.statusCode();
List arguments = getExposedValues(exception);
- return new HandledException(errorCode, httpStatus, singletonMap(errorCode, arguments));
+ return new HandledException(new ErrorWithArguments(errorCode, arguments), httpStatus);
}
/**
diff --git a/src/main/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandler.java
index 4170b8a..59bdc69 100644
--- a/src/main/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandler.java
@@ -1,6 +1,6 @@
package me.alidg.errors.handlers;
-import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.http.HttpStatus;
@@ -9,11 +9,9 @@
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Collectors.*;
/**
* A {@link WebErrorHandler} implementation responsible for handling {@link ConstraintViolationException}s
@@ -54,10 +52,8 @@ public boolean canHandle(Throwable exception) {
@Override
public HandledException handle(Throwable exception) {
ConstraintViolationException violationException = (ConstraintViolationException) exception;
- Set errorCodes = extractErrorCodes(violationException);
- Map> arguments = extractArguments(violationException);
- return new HandledException(errorCodes, HttpStatus.BAD_REQUEST, arguments);
+ return new HandledException(extractErrorWithArguments(violationException), HttpStatus.BAD_REQUEST);
}
/**
@@ -72,36 +68,13 @@ private boolean hasAtLeastOneViolation(Throwable exception) {
return violations != null && !violations.isEmpty();
}
- /**
- * Extract annotation attributes (except for those three mandatory attributes) and expose them as arguments.
- *
- * @param violationException The exception to extract the arguments from.
- * @return To-be-exposed arguments.
- */
- private Map> extractArguments(ConstraintViolationException violationException) {
- Map> args = violationException
- .getConstraintViolations()
- .stream()
- .collect(toMap(ConstraintViolations::getErrorCode, ConstraintViolations::getArguments, (v1, v2) -> v1));
- args.entrySet().removeIf(e -> e.getValue().isEmpty());
- return args;
- }
-
- /**
- * Extract message templates and use them as error codes.
- *
- * @param violationException The exception to extract the error codes from.
- * @return A set of error codes.
- */
- private Set extractErrorCodes(ConstraintViolationException violationException) {
- return violationException
+ private List extractErrorWithArguments(ConstraintViolationException exception) {
+ return exception
.getConstraintViolations()
.stream()
- .map(this::errorCode)
- .collect(toSet());
- }
-
- private String errorCode(ConstraintViolation> violation) {
- return violation.getMessageTemplate().replace("{", "").replace("}", "");
+ .map(constraintViolation -> new ErrorWithArguments(ConstraintViolations.getErrorCode(constraintViolation),
+ ConstraintViolations.getArguments(constraintViolation)))
+ .filter(errorWithArguments -> !errorWithArguments.getArguments().isEmpty())
+ .collect(toList());
}
}
diff --git a/src/main/java/me/alidg/errors/handlers/LastResortWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/LastResortWebErrorHandler.java
index 1744345..1d33951 100644
--- a/src/main/java/me/alidg/errors/handlers/LastResortWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/LastResortWebErrorHandler.java
@@ -1,11 +1,10 @@
package me.alidg.errors.handlers;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.http.HttpStatus;
-import java.util.Collections;
-
/**
* The default fallback {@link WebErrorHandler} which will be used when all
* other registered handlers refuse to handle a particular exception. You can
@@ -46,6 +45,6 @@ public boolean canHandle(Throwable exception) {
*/
@Override
public HandledException handle(Throwable exception) {
- return new HandledException(UNKNOWN_ERROR_CODE, HttpStatus.INTERNAL_SERVER_ERROR, Collections.emptyMap());
+ return new HandledException(ErrorWithArguments.noArgumentError(UNKNOWN_ERROR_CODE), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
diff --git a/src/main/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandler.java
index e1cc403..f03aff2 100644
--- a/src/main/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.core.MethodParameter;
@@ -12,7 +13,6 @@
import java.util.ArrayList;
import java.util.List;
-import static java.util.Collections.singletonMap;
import static me.alidg.errors.Argument.arg;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@@ -91,7 +91,7 @@ public HandledException handle(Throwable exception) {
errorCode = MISSING_MATRIX_VARIABLE;
}
- return new HandledException(errorCode, BAD_REQUEST, singletonMap(errorCode, arguments));
+ return new HandledException(new ErrorWithArguments(errorCode, arguments), BAD_REQUEST);
}
private String getType(MethodParameter parameter) {
diff --git a/src/main/java/me/alidg/errors/handlers/MultipartWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/MultipartWebErrorHandler.java
index 4dc4552..51ba58a 100644
--- a/src/main/java/me/alidg/errors/handlers/MultipartWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/MultipartWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.lang.NonNull;
@@ -8,7 +9,6 @@
import org.springframework.web.multipart.MultipartException;
import java.util.List;
-import java.util.Map;
import static java.util.Collections.*;
import static me.alidg.errors.Argument.arg;
@@ -47,14 +47,14 @@ public boolean canHandle(Throwable exception) {
@Override
public HandledException handle(Throwable exception) {
String errorCode = MULTIPART_EXPECTED;
- Map> arguments = emptyMap();
+ List arguments = emptyList();
if (exception instanceof MaxUploadSizeExceededException) {
long maxSize = ((MaxUploadSizeExceededException) exception).getMaxUploadSize();
errorCode = MAX_SIZE;
- arguments = singletonMap(MAX_SIZE, singletonList(arg("max_size", maxSize)));
+ arguments = singletonList(arg("max_size", maxSize));
}
- return new HandledException(errorCode, BAD_REQUEST, arguments);
+ return new HandledException(new ErrorWithArguments(errorCode, arguments), BAD_REQUEST);
}
}
diff --git a/src/main/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandler.java
index 982a196..79c7025 100644
--- a/src/main/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.beans.TypeMismatchException;
@@ -66,25 +67,27 @@ public boolean canHandle(Throwable exception) {
public HandledException handle(Throwable exception) {
if (exception instanceof MediaTypeNotSupportedStatusException) {
Set types = getMediaTypes(((MediaTypeNotSupportedStatusException) exception).getSupportedMediaTypes());
- Map> args = types.isEmpty() ? emptyMap() : argMap(NOT_SUPPORTED, arg("types", types));
- return new HandledException(NOT_SUPPORTED, UNSUPPORTED_MEDIA_TYPE, args);
+ List args = types.isEmpty() ? emptyList() : singletonList(arg("types", types));
+ return new HandledException(new ErrorWithArguments(NOT_SUPPORTED, args),
+ UNSUPPORTED_MEDIA_TYPE);
}
if (exception instanceof UnsupportedMediaTypeStatusException) {
Set types = getMediaTypes(((UnsupportedMediaTypeStatusException) exception).getSupportedMediaTypes());
- Map> args = types.isEmpty() ? emptyMap() : argMap(NOT_SUPPORTED, arg("types", types));
- return new HandledException(NOT_SUPPORTED, UNSUPPORTED_MEDIA_TYPE, args);
+ List args = types.isEmpty() ? emptyList() : singletonList(arg("types", types));
+ return new HandledException(new ErrorWithArguments(NOT_SUPPORTED, args), UNSUPPORTED_MEDIA_TYPE);
}
if (exception instanceof NotAcceptableStatusException) {
Set types = getMediaTypes(((NotAcceptableStatusException) exception).getSupportedMediaTypes());
- Map> args = types.isEmpty() ? emptyMap() : argMap(NOT_ACCEPTABLE, arg("types", types));
- return new HandledException(NOT_ACCEPTABLE, HttpStatus.NOT_ACCEPTABLE, args);
+ List args = types.isEmpty() ? emptyList() : singletonList(arg("types", types));
+ return new HandledException(new ErrorWithArguments(NOT_ACCEPTABLE, args), HttpStatus.NOT_ACCEPTABLE);
}
if (exception instanceof MethodNotAllowedException) {
String httpMethod = ((MethodNotAllowedException) exception).getHttpMethod();
- return new HandledException(METHOD_NOT_ALLOWED, HttpStatus.METHOD_NOT_ALLOWED, argMap(METHOD_NOT_ALLOWED, arg("method", httpMethod)));
+ return new HandledException(new ErrorWithArguments(METHOD_NOT_ALLOWED, singletonList(arg("method", httpMethod))),
+ HttpStatus.METHOD_NOT_ALLOWED);
}
if (exception instanceof WebExchangeBindException) {
@@ -103,17 +106,17 @@ public HandledException handle(Throwable exception) {
HandledException handledException = handleMissingParameters(parameter);
if (handledException != null) return handledException;
- return new HandledException(INVALID_OR_MISSING_BODY, BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(INVALID_OR_MISSING_BODY), BAD_REQUEST);
}
if (exception instanceof ResponseStatusException) {
HttpStatus status = ((ResponseStatusException) exception).getStatus();
- if (status == NOT_FOUND) return new HandledException(NO_HANDLER, status, null);
+ if (status == NOT_FOUND) return new HandledException(ErrorWithArguments.noArgumentError(NO_HANDLER), status);
- return new HandledException(UNKNOWN_ERROR_CODE, status, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(UNKNOWN_ERROR_CODE), status);
}
- return new HandledException(UNKNOWN_ERROR_CODE, INTERNAL_SERVER_ERROR, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(UNKNOWN_ERROR_CODE), INTERNAL_SERVER_ERROR);
}
/**
@@ -164,8 +167,8 @@ private HandledException handleMissingParameters(MethodParameter parameter) {
}
if (code != null) {
- return new HandledException(code, BAD_REQUEST,
- argMap(code, arg("name", parameterName), arg("expected", Classes.getClassName(parameter.getParameterType()))));
+ List arguments = asList(arg("name", parameterName), arg("expected", Classes.getClassName(parameter.getParameterType())));
+ return new HandledException(new ErrorWithArguments(code, arguments), BAD_REQUEST);
}
return null;
diff --git a/src/main/java/me/alidg/errors/handlers/ServletWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/ServletWebErrorHandler.java
index a71d28b..649eab0 100644
--- a/src/main/java/me/alidg/errors/handlers/ServletWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/ServletWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.http.HttpStatus;
@@ -16,7 +17,6 @@
import javax.servlet.ServletException;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import static java.util.Arrays.asList;
@@ -97,14 +97,15 @@ public boolean canHandle(Throwable exception) {
@Override
public HandledException handle(Throwable exception) {
if (exception instanceof HttpMessageNotReadableException)
- return new HandledException(INVALID_OR_MISSING_BODY, HttpStatus.BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(INVALID_OR_MISSING_BODY), HttpStatus.BAD_REQUEST);
if (exception instanceof HttpMediaTypeNotAcceptableException) {
Set types = getMediaTypes(((HttpMediaTypeNotAcceptableException) exception).getSupportedMediaTypes());
- Map> args = types.isEmpty() ?
- emptyMap() : singletonMap(NOT_ACCEPTABLE, singletonList(arg("types", types)));
+ List args = types.isEmpty() ?
+ emptyList() : singletonList(arg("types", types));
- return new HandledException(NOT_ACCEPTABLE, HttpStatus.NOT_ACCEPTABLE, args);
+ return new HandledException(new ErrorWithArguments(NOT_ACCEPTABLE, args),
+ HttpStatus.NOT_ACCEPTABLE);
}
if (exception instanceof HttpMediaTypeNotSupportedException) {
@@ -112,16 +113,15 @@ public HandledException handle(Throwable exception) {
List arguments = null;
if (contentType != null) arguments = singletonList(arg("type", contentType.toString()));
- return new HandledException(NOT_SUPPORTED, HttpStatus.UNSUPPORTED_MEDIA_TYPE,
- arguments == null ? emptyMap() : singletonMap(NOT_SUPPORTED, arguments));
+ return new HandledException(new ErrorWithArguments(NOT_SUPPORTED, arguments),
+ HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
if (exception instanceof HttpRequestMethodNotSupportedException) {
String method = ((HttpRequestMethodNotSupportedException) exception).getMethod();
- return new HandledException(METHOD_NOT_ALLOWED, HttpStatus.METHOD_NOT_ALLOWED,
- singletonMap(METHOD_NOT_ALLOWED, singletonList(arg("method", method)))
- );
+ return new HandledException(new ErrorWithArguments(METHOD_NOT_ALLOWED, singletonList(arg("method", method))),
+ HttpStatus.METHOD_NOT_ALLOWED);
}
if (exception instanceof MissingServletRequestParameterException) {
@@ -129,28 +129,26 @@ public HandledException handle(Throwable exception) {
String name = actualException.getParameterName();
String type = actualException.getParameterType();
- return new HandledException(MISSING_PARAMETER, HttpStatus.BAD_REQUEST,
- singletonMap(MISSING_PARAMETER, asList(arg("name", name), arg("expected", type)))
- );
+ return new HandledException(new ErrorWithArguments(MISSING_PARAMETER,
+ asList(arg("name", name), arg("expected", type))),
+ HttpStatus.BAD_REQUEST);
}
if (exception instanceof MissingServletRequestPartException) {
String name = ((MissingServletRequestPartException) exception).getRequestPartName();
- return new HandledException(MISSING_PART, HttpStatus.BAD_REQUEST,
- singletonMap(MISSING_PART, singletonList(arg("name", name)))
- );
+ return new HandledException(new ErrorWithArguments(MISSING_PART, singletonList(arg("name", name))),
+ HttpStatus.BAD_REQUEST);
}
if (exception instanceof NoHandlerFoundException) {
String url = ((NoHandlerFoundException) exception).getRequestURL();
- return new HandledException(NO_HANDLER, HttpStatus.NOT_FOUND,
- singletonMap(NO_HANDLER, singletonList(arg("path", url)))
- );
+ return new HandledException(new ErrorWithArguments(NO_HANDLER, singletonList(arg("path", url))),
+ HttpStatus.NOT_FOUND);
}
- return new HandledException("unknown_error", HttpStatus.INTERNAL_SERVER_ERROR, null);
+ return new HandledException(ErrorWithArguments.noArgumentError("unknown_error"), HttpStatus.INTERNAL_SERVER_ERROR);
}
private Set getMediaTypes(List mediaTypes) {
diff --git a/src/main/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandler.java
index 94750c4..6120217 100644
--- a/src/main/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandler.java
@@ -1,5 +1,6 @@
package me.alidg.errors.handlers;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.lang.NonNull;
@@ -82,29 +83,29 @@ public boolean canHandle(Throwable exception) {
@Override
public HandledException handle(Throwable exception) {
if (exception instanceof AccessDeniedException)
- return new HandledException(ACCESS_DENIED, FORBIDDEN, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(ACCESS_DENIED), FORBIDDEN);
if (exception instanceof AccountExpiredException)
- return new HandledException(ACCOUNT_EXPIRED, BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(ACCOUNT_EXPIRED), BAD_REQUEST);
if (exception instanceof AuthenticationCredentialsNotFoundException)
- return new HandledException(AUTH_REQUIRED, UNAUTHORIZED, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(AUTH_REQUIRED), UNAUTHORIZED);
if (exception instanceof AuthenticationServiceException)
- return new HandledException(INTERNAL_ERROR, INTERNAL_SERVER_ERROR, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(INTERNAL_ERROR), INTERNAL_SERVER_ERROR);
if (exception instanceof BadCredentialsException)
- return new HandledException(BAD_CREDENTIALS, BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(BAD_CREDENTIALS), BAD_REQUEST);
if (exception instanceof UsernameNotFoundException)
- return new HandledException(BAD_CREDENTIALS, BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(BAD_CREDENTIALS), BAD_REQUEST);
if (exception instanceof InsufficientAuthenticationException)
- return new HandledException(AUTH_REQUIRED, UNAUTHORIZED, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(AUTH_REQUIRED), UNAUTHORIZED);
- if (exception instanceof LockedException) return new HandledException(USER_LOCKED, BAD_REQUEST, null);
- if (exception instanceof DisabledException) return new HandledException(USER_DISABLED, BAD_REQUEST, null);
+ if (exception instanceof LockedException) return new HandledException(ErrorWithArguments.noArgumentError(USER_LOCKED), BAD_REQUEST);
+ if (exception instanceof DisabledException) return new HandledException(ErrorWithArguments.noArgumentError(USER_DISABLED), BAD_REQUEST);
- return new HandledException("unknown_error", INTERNAL_SERVER_ERROR, null);
+ return new HandledException(ErrorWithArguments.noArgumentError("unknown_error"), INTERNAL_SERVER_ERROR);
}
}
diff --git a/src/main/java/me/alidg/errors/handlers/SpringValidationWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/SpringValidationWebErrorHandler.java
index d4aa5a6..73aebea 100644
--- a/src/main/java/me/alidg/errors/handlers/SpringValidationWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/SpringValidationWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.beans.TypeMismatchException;
@@ -13,10 +14,9 @@
import javax.validation.ConstraintViolation;
import java.util.List;
-import java.util.Map;
+import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
-import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toMap;
/**
@@ -58,12 +58,12 @@ public boolean canHandle(Throwable exception) {
@Override
public HandledException handle(Throwable exception) {
BindingResult bindingResult = getBindingResult(exception);
- return bindingResult.getAllErrors()
- .stream()
- .collect(collectingAndThen(
- toMap(this::errorCode, this::arguments, (value1, value2) -> value1),
- m -> new HandledException(m.keySet(), HttpStatus.BAD_REQUEST, dropEmptyValues(m))
- ));
+ List errorWithArguments = bindingResult.getAllErrors()
+ .stream()
+ .map(objectError -> new ErrorWithArguments(errorCode(objectError),
+ arguments(objectError)))
+ .collect(Collectors.toList());
+ return new HandledException(errorWithArguments, HttpStatus.BAD_REQUEST);
}
/**
@@ -126,16 +126,4 @@ private List arguments(ObjectError error) {
return emptyList();
}
-
- /**
- * Drops the empty collection of arguments!
- *
- * @param input The error code to arguments map.
- * @return The filtered map.
- */
- private Map> dropEmptyValues(Map> input) {
- return input.entrySet().stream()
- .filter(e -> e.getValue() != null && !e.getValue().isEmpty())
- .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2));
- }
}
diff --git a/src/main/java/me/alidg/errors/handlers/TypeMismatchWebErrorHandler.java b/src/main/java/me/alidg/errors/handlers/TypeMismatchWebErrorHandler.java
index eae1e60..28a2363 100644
--- a/src/main/java/me/alidg/errors/handlers/TypeMismatchWebErrorHandler.java
+++ b/src/main/java/me/alidg/errors/handlers/TypeMismatchWebErrorHandler.java
@@ -1,6 +1,7 @@
package me.alidg.errors.handlers;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.springframework.beans.TypeMismatchException;
@@ -10,7 +11,6 @@
import java.util.ArrayList;
import java.util.List;
-import static java.util.Collections.singletonMap;
import static me.alidg.errors.Argument.arg;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@@ -51,7 +51,7 @@ public HandledException handle(Throwable exception) {
String errorCode = getErrorCode(mismatchException);
List arguments = getArguments(mismatchException);
- return new HandledException(errorCode, BAD_REQUEST, singletonMap(errorCode, arguments));
+ return new HandledException(new ErrorWithArguments(errorCode, arguments), BAD_REQUEST);
}
static List getArguments(TypeMismatchException mismatchException) {
diff --git a/src/test/java/me/alidg/errors/HandledExceptionTest.java b/src/test/java/me/alidg/errors/HandledExceptionTest.java
index fa85a49..7cb8778 100644
--- a/src/test/java/me/alidg/errors/HandledExceptionTest.java
+++ b/src/test/java/me/alidg/errors/HandledExceptionTest.java
@@ -6,16 +6,13 @@
import org.junit.runner.RunWith;
import org.springframework.http.HttpStatus;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import static java.util.Arrays.asList;
-import static java.util.Collections.*;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
import static me.alidg.Params.p;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.*;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
@@ -28,11 +25,11 @@ public class HandledExceptionTest {
@Test
@Parameters(method = "provideParamsForPrimary")
- public void primaryConstructor_ShouldEnforceItsPreconditions(Set errorCodes,
+ public void primaryConstructor_ShouldEnforceItsPreconditions(List errors,
HttpStatus status,
Class extends Throwable> expected,
String message) {
- assertThatThrownBy(() -> new HandledException(errorCodes, status, singletonMap("error", emptyList())))
+ assertThatThrownBy(() -> new HandledException(errors, status))
.isInstanceOf(expected)
.hasMessage(message);
}
@@ -43,43 +40,82 @@ public void secondConstructor_ShouldEnforceItsPreconditions(String errorCode,
HttpStatus status,
Class extends Throwable> expected,
String message) {
- assertThatThrownBy(() -> new HandledException(errorCode, status, singletonMap("error", emptyList())))
+ assertThatThrownBy(() -> new HandledException(ErrorWithArguments.noArgumentError(errorCode), status))
.isInstanceOf(expected)
.hasMessage(message);
}
@Test
- @Parameters(method = "provideMaps")
- public void constructors_ShouldSetNullArgumentsAsEmptyMaps(Map> provided,
- Map, ?> expected) {
- assertThat(new HandledException(singleton("error"), BAD_REQUEST, provided).getArguments())
- .isEqualTo(expected);
-
- assertThat(new HandledException("error", BAD_REQUEST, provided).getArguments())
- .isEqualTo(expected);
+ public void testGetErrorCodes_singleError() {
+ HandledException exception = new HandledException(ErrorWithArguments.noArgumentError("error"), BAD_REQUEST);
+ assertThat(exception.getErrorCodes()).hasSize(1)
+ .contains("error");
+
+ }
+
+ @Test
+ public void testGetErrorCodes_multipleErrors() {
+ List list = asList(ErrorWithArguments.noArgumentError("error"),
+ new ErrorWithArguments("error2", singletonList(Argument.arg("argName", "argValue"))));
+ HandledException exception = new HandledException(list, BAD_REQUEST);
+ assertThat(exception.getErrorCodes()).hasSize(2)
+ .contains("error", "error2");
+
+ }
+
+ @Test
+ public void testGetErrorCodes_duplicateErrors() {
+ List list = asList(new ErrorWithArguments("error", singletonList(Argument.arg("argName", "argValue1"))),
+ new ErrorWithArguments("error", singletonList(Argument.arg("argName", "argValue2"))));
+ HandledException exception = new HandledException(list, BAD_REQUEST);
+ assertThat(exception.getErrorCodes()).hasSize(1)
+ .contains("error");
+
+ }
+
+ @Test
+ public void testGetArguments_singleError() {
+ HandledException exception = new HandledException(ErrorWithArguments.noArgumentError("error"), BAD_REQUEST);
+ assertThat(exception.getArguments()).hasSize(1)
+ .hasEntrySatisfying("error", arguments -> assertThat(arguments).isEmpty());
+
+ }
+
+ @Test
+ public void testGetArguments_multipleErrors() {
+ List list = asList(ErrorWithArguments.noArgumentError("error"),
+ new ErrorWithArguments("error2", singletonList(Argument.arg("argName", "argValue"))));
+ HandledException exception = new HandledException(list, BAD_REQUEST);
+ assertThat(exception.getArguments()).hasSize(2)
+ .hasEntrySatisfying("error", arguments -> assertThat(arguments).isEmpty())
+ .hasEntrySatisfying("error2", arguments -> assertThat(arguments).hasSize(1));
+
+ }
+
+ @Test
+ public void testGetArguments_duplicateErrors() {
+ List list = asList(new ErrorWithArguments("error", singletonList(Argument.arg("argName", "argValue1"))),
+ new ErrorWithArguments("error", singletonList(Argument.arg("argName", "argValue2"))));
+ HandledException exception = new HandledException(list, BAD_REQUEST);
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(exception::getArguments)
+ .withMessage("Duplicate key error (attempted merging values [argName=argValue1] and [argName=argValue2])");
}
private Object[] provideParamsForPrimary() {
return p(
p(null, null, NullPointerException.class, "Error codes is required"),
- p(new HashSet<>(asList("", "", null)), null, NullPointerException.class, "Status code is required"),
- p(singleton(null), BAD_REQUEST, NullPointerException.class, "The single error code can't be null"),
- p(emptySet(), BAD_REQUEST, IllegalArgumentException.class, "At least one error code should be provided")
+ p(asList(ErrorWithArguments.noArgumentError(""), ErrorWithArguments.noArgumentError(""), null), null, NullPointerException.class, "Status code is required"),
+ p(singletonList(null), BAD_REQUEST, NullPointerException.class, "The single error code can't be null"),
+ p(emptyList(), BAD_REQUEST, IllegalArgumentException.class, "At least one error code should be provided")
);
}
private Object[] provideParamsForSecondary() {
return p(
- p(null, null, NullPointerException.class, "Status code is required"),
+ p(null, null, NullPointerException.class, "The single error code can't be null"),
p("error", null, NullPointerException.class, "Status code is required"),
p(null, BAD_REQUEST, NullPointerException.class, "The single error code can't be null")
);
}
-
- private Object[] provideMaps() {
- return p(
- p(null, emptyMap()),
- p(singletonMap("key", emptyList()), singletonMap("key", emptyList()))
- );
- }
}
diff --git a/src/test/java/me/alidg/errors/conf/ErrorsAutoConfigurationIT.java b/src/test/java/me/alidg/errors/conf/ErrorsAutoConfigurationIT.java
index c541add..6280714 100644
--- a/src/test/java/me/alidg/errors/conf/ErrorsAutoConfigurationIT.java
+++ b/src/test/java/me/alidg/errors/conf/ErrorsAutoConfigurationIT.java
@@ -2,6 +2,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import me.alidg.errors.WebErrorHandlers;
@@ -294,7 +295,7 @@ public boolean canHandle(Throwable exception) {
@Override
@NonNull
public HandledException handle(Throwable exception) {
- return new HandledException("", HttpStatus.BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(""), HttpStatus.BAD_REQUEST);
}
}
@@ -308,7 +309,7 @@ public boolean canHandle(Throwable exception) {
@Override
@NonNull
public HandledException handle(Throwable exception) {
- return new HandledException("", HttpStatus.BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(""), HttpStatus.BAD_REQUEST);
}
}
@@ -322,7 +323,7 @@ public boolean canHandle(Throwable exception) {
@Override
@NonNull
public HandledException handle(Throwable exception) {
- return new HandledException("", HttpStatus.BAD_REQUEST, null);
+ return new HandledException(ErrorWithArguments.noArgumentError(""), HttpStatus.BAD_REQUEST);
}
}
}
diff --git a/src/test/java/me/alidg/errors/handlers/AnnotatedWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/AnnotatedWebErrorHandlerTest.java
index a532df5..8e44b2b 100644
--- a/src/test/java/me/alidg/errors/handlers/AnnotatedWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/AnnotatedWebErrorHandlerTest.java
@@ -3,6 +3,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.annotation.ExceptionMapping;
import me.alidg.errors.annotation.ExposeAsArg;
@@ -49,10 +50,14 @@ public void handle_ShouldProperlyHandleTheGivenException(Exception exception,
HandledException handled = handler.handle(exception);
assertThat(handled).isNotNull();
- assertThat(handled.getErrorCodes()).hasSize(1);
- assertThat(handled.getErrorCodes()).containsExactly(code);
+ List errors = handled.getErrors();
+ assertThat(errors).hasSize(1)
+ .extracting(ErrorWithArguments::getErrorCode)
+ .containsExactly(code);
+ assertThat(errors).hasSize(1)
+ .extracting(ErrorWithArguments::getArguments)
+ .containsExactly(args);
assertThat(handled.getStatusCode()).isEqualTo(status);
- assertThat((handled.getArguments().get(code))).isEqualTo(args);
}
private Object[] provideParamsForCanHandle() {
diff --git a/src/test/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandlerTest.java
index 6ff5c60..3c088fb 100644
--- a/src/test/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/ConstraintViolationWebErrorHandlerTest.java
@@ -3,6 +3,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.junit.Test;
@@ -21,8 +22,7 @@
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
-import static java.util.Collections.singleton;
-import static java.util.Collections.singletonMap;
+import static java.util.Collections.*;
import static me.alidg.Params.p;
import static me.alidg.errors.Argument.arg;
import static org.assertj.core.api.Assertions.assertThat;
@@ -61,8 +61,11 @@ public void handle_ShouldHandleViolationExceptionsProperly(ConstraintViolationEx
HandledException handledException = handler.handle(exception);
assertThat(handledException.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(handledException.getErrorCodes()).containsAll(errorCodes);
- assertThat(handledException.getArguments()).containsAllEntriesOf(arguments);
+ List errors = handledException.getErrors();
+ assertThat(errors).extracting(ErrorWithArguments::getErrorCode)
+ .containsExactlyInAnyOrderElementsOf(errorCodes);
+ assertThat(errors).extracting(ErrorWithArguments::getArguments)
+ .containsExactlyInAnyOrderElementsOf(arguments.values());
}
@SuppressWarnings("unchecked")
@@ -92,11 +95,16 @@ private Object[] provideParamsForHandle() {
p(
v(new Person("", 19)),
setOf("username.blank", "username.size"),
- singletonMap("username.size", asList(
- arg("max", 10),
- arg("min", 6),
- arg("invalid", ""),
- arg("property", "username")))
+ new HashMap>() {{
+ put("username.size", asList(
+ arg("max", 10),
+ arg("min", 6),
+ arg("invalid", ""),
+ arg("property", "username")));
+ put("username.blank", asList(
+ arg("invalid", ""),
+ arg("property", "username")));
+ }}
),
p(
v(new Person("ali", 12)),
diff --git a/src/test/java/me/alidg/errors/handlers/LastResortWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/LastResortWebErrorHandlerTest.java
index 9d4edc5..9cace99 100644
--- a/src/test/java/me/alidg/errors/handlers/LastResortWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/LastResortWebErrorHandlerTest.java
@@ -2,17 +2,19 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.http.HttpStatus;
-import static java.util.Collections.singleton;
+import static java.util.Collections.emptyList;
import static me.alidg.Params.p;
import static me.alidg.errors.handlers.LastResortWebErrorHandler.INSTANCE;
import static me.alidg.errors.handlers.LastResortWebErrorHandler.UNKNOWN_ERROR_CODE;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
/**
* Unit tests for {@link LastResortWebErrorHandler} exception handler.
@@ -39,9 +41,12 @@ public void canHandle_AlwaysReturnsFalse(Throwable exception) {
public void handle_AlwaysReturn500InternalErrorWithStaticErrorCode(Throwable exception) {
HandledException handled = handler.handle(exception);
- assertThat(handled.getErrorCodes()).isEqualTo(singleton(UNKNOWN_ERROR_CODE));
assertThat(handled.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
- assertThat(handled.getArguments()).isEmpty();
+ assertThat(handled.getErrors()).extracting(ErrorWithArguments::getErrorCode,
+ ErrorWithArguments::getArguments)
+ .containsOnly(tuple(UNKNOWN_ERROR_CODE,
+ emptyList()));
+
}
private Object[] provideParams() {
diff --git a/src/test/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandlerTest.java
index 7013a3d..9a9d371 100644
--- a/src/test/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/MissingRequestParametersWebErrorHandlerTest.java
@@ -2,6 +2,8 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
+import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import me.alidg.errors.WebErrorHandler;
import org.junit.Test;
@@ -51,12 +53,16 @@ public void canHandle_ShouldReturnTrueForMissingRequestParamsErrors(Throwable ex
public void handle_ShouldHandleMissingRequestParamsErrorsProperly(Throwable exception,
String expectedCode,
HttpStatus expectedStatus,
- Map> expectedArgs) {
+ Map> expectedArgs) {
HandledException handledException = handler.handle(exception);
- assertThat(handledException.getErrorCodes()).containsOnly(expectedCode);
+ List errors = handledException.getErrors();
+ assertThat(errors).extracting(ErrorWithArguments::getErrorCode)
+ .containsOnly(expectedCode);
+ assertThat(errors).extracting(ErrorWithArguments::getArguments)
+ .containsExactlyInAnyOrderElementsOf(expectedArgs.values());
+
assertThat(handledException.getStatusCode()).isEqualTo(expectedStatus);
- assertThat(handledException.getArguments()).isEqualTo(expectedArgs);
}
private Object[] provideParamsForCanHandle() {
diff --git a/src/test/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandlerTest.java
index ad6e8b3..e08c31a 100644
--- a/src/test/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/ResponseStatusWebErrorHandlerTest.java
@@ -3,6 +3,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -15,9 +16,7 @@
import org.springframework.web.server.*;
import java.lang.annotation.Annotation;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
+import java.util.*;
import static java.util.Collections.*;
import static me.alidg.Params.p;
@@ -55,12 +54,18 @@ public void handle_ShouldHandleResponseStatusExceptionsAppropriately(Exception e
List expectedArguments) {
HandledException handledException = handler.handle(e);
- assertThat(handledException.getErrorCodes()).containsExactly(expectedErrorCode);
+ List errors = handledException.getErrors();
+ assertThat(errors).extracting(ErrorWithArguments::getErrorCode)
+ .containsOnly(expectedErrorCode);
+ // We need to sort the list of arguments to be able to compare them
+ Collections.sort(expectedArguments, Comparator.comparing(Argument::getName));
+ assertThat(errors).extracting(errorWithArguments -> {
+ List arguments = errorWithArguments.getArguments();
+ Collections.sort(arguments, Comparator.comparing(Argument::getName));
+ return arguments;
+ }).containsExactlyInAnyOrder(expectedArguments);
+
assertThat(handledException.getStatusCode()).isEqualTo(expectedStatus);
- if (expectedArguments == null || expectedArguments.isEmpty())
- assertThat(handledException.getArguments()).isEmpty();
- else
- assertThat(handledException.getArguments().get(expectedErrorCode)).containsAll(expectedArguments);
}
private Object[] paramsForCanHandle() {
diff --git a/src/test/java/me/alidg/errors/handlers/ServletWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/ServletWebErrorHandlerTest.java
index 34abef8..5a1f546 100644
--- a/src/test/java/me/alidg/errors/handlers/ServletWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/ServletWebErrorHandlerTest.java
@@ -2,6 +2,8 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
+import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,12 +58,22 @@ public void canHandle_ShouldReturnTrueForSpringMvcSpecificErrors(Throwable excep
public void handle_ShouldHandleSpringMvcErrorsProperly(Throwable exception,
String expectedCode,
HttpStatus expectedStatus,
- Map> expectedArgs) {
+ Map> expectedArgs) {
HandledException handledException = handler.handle(exception);
- assertThat(handledException.getErrorCodes()).containsOnly(expectedCode);
+ List errors = handledException.getErrors();
+ assertThat(errors).extracting(ErrorWithArguments::getErrorCode)
+ .containsExactly(expectedCode);
+ if (expectedArgs.isEmpty()) {
+ assertThat(errors).extracting(ErrorWithArguments::getArguments)
+ .extracting(List::size)
+ .containsExactly(0);
+ } else {
+ assertThat(errors).extracting(ErrorWithArguments::getArguments)
+ .containsExactlyInAnyOrderElementsOf(expectedArgs.values());
+ }
+
assertThat(handledException.getStatusCode()).isEqualTo(expectedStatus);
- assertThat(handledException.getArguments()).isEqualTo(expectedArgs);
}
private Object[] provideParamsForCanHandle() {
diff --git a/src/test/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandlerTest.java
index 6ddbb1f..f10baae 100644
--- a/src/test/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/SpringSecurityWebErrorHandlerTest.java
@@ -2,6 +2,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -11,9 +12,11 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.rememberme.CookieTheftException;
+import static java.util.Collections.emptyList;
import static me.alidg.Params.p;
import static me.alidg.errors.handlers.SpringSecurityWebErrorHandler.*;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
import static org.springframework.http.HttpStatus.*;
/**
@@ -44,8 +47,10 @@ public void handle_ShouldHandleSecurityRelatedExceptionsProperly(Throwable excep
HandledException handled = handler.handle(exception);
assertThat(handled.getStatusCode()).isEqualTo(expectedStatusCode);
- assertThat(handled.getErrorCodes()).containsOnly(expectedErrorCode);
- assertThat(handled.getArguments()).isEmpty();
+ assertThat(handled.getErrors()).extracting(ErrorWithArguments::getErrorCode,
+ ErrorWithArguments::getArguments)
+ .containsOnly(tuple(expectedErrorCode,
+ emptyList()));
}
private Object[] provideParamsForCanHandle() {
diff --git a/src/test/java/me/alidg/errors/handlers/SpringValidationWebErrorHandlerTest.java b/src/test/java/me/alidg/errors/handlers/SpringValidationWebErrorHandlerTest.java
index 136792f..c1e02aa 100644
--- a/src/test/java/me/alidg/errors/handlers/SpringValidationWebErrorHandlerTest.java
+++ b/src/test/java/me/alidg/errors/handlers/SpringValidationWebErrorHandlerTest.java
@@ -3,6 +3,7 @@
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import me.alidg.errors.Argument;
+import me.alidg.errors.ErrorWithArguments;
import me.alidg.errors.HandledException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -13,10 +14,7 @@
import javax.validation.Valid;
import javax.validation.Validation;
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Size;
+import javax.validation.constraints.*;
import java.util.*;
import static java.util.Arrays.asList;
@@ -27,6 +25,7 @@
import static me.alidg.errors.handlers.SpringValidationWebErrorHandlerTest.TBV.tbv;
import static me.alidg.errors.handlers.SpringValidationWebErrorHandlerTest.TBVchild.tbvChild;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -60,8 +59,7 @@ public void canHandle_ShouldOnlyReturnTrueForSpringSpecificValidationErrors(Thro
@Test
@Parameters(method = "provideParamsForHandle")
public void handle_ShouldHandleTheValidationErrorsProperly(Object toValidate,
- Set errorCodes,
- Map> args) {
+ List errors) {
BindingResult result = new BeanPropertyBindingResult(toValidate, "toValidate");
validator.validate(toValidate, result);
@@ -70,18 +68,16 @@ public void handle_ShouldHandleTheValidationErrorsProperly(Object toValidate,
BindException bindException = new BindException(result);
HandledException handledForBind = handler.handle(bindException);
- assertThat(handledForBind.getErrorCodes()).containsAll(errorCodes);
+ assertThat(handledForBind.getErrors()).containsAll(errors);
assertThat(handledForBind.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(handledForBind.getArguments()).isEqualTo(args);
// Create and assert for MethodArgumentNotValidException
MethodArgumentNotValidException exception = new MethodArgumentNotValidException(null, result);
HandledException handled = handler.handle(exception);
- assertThat(handled.getErrorCodes()).containsAll(errorCodes);
+ assertThat(handled.getErrors()).containsAll(errors);
assertThat(handled.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(handled.getArguments()).isEqualTo(args);
}
@Test
@@ -91,9 +87,43 @@ public void handle_ForUnknownBindingErrorsShouldReturnBindingFailureErrorCode()
BindException exception = new BindException(bindingResult);
HandledException handledException = handler.handle(exception);
- assertThat(handledException.getArguments()).isEmpty();
assertThat(handledException.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(handledException.getErrorCodes()).containsOnly(BINDING_FAILURE);
+ assertThat(handledException.getErrors()).extracting(ErrorWithArguments::getErrorCode,
+ ErrorWithArguments::getArguments)
+ .containsOnly(tuple(BINDING_FAILURE,
+ emptyList()));
+ }
+
+ @Test
+ public void testWithMultipleSameErrorCodes() {
+ UserCreationParameters toValidate = new UserCreationParameters();
+ BindingResult result = new BeanPropertyBindingResult(toValidate, "toValidate");
+ validator.validate(toValidate, result);
+
+ BindException bindException = new BindException(result);
+ HandledException handledForBind = handler.handle(bindException);
+
+ assertThat(handledForBind.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(handledForBind.getErrors()).hasSize(4)
+ .extracting(ErrorWithArguments::getErrorCode,
+ ErrorWithArguments::getArguments)
+ .containsExactlyInAnyOrder(
+ tuple("property.should.not.be.empty",
+ asList(Argument.arg("invalid", null),
+ Argument.arg("property", "firstName"))),
+ tuple("property.should.not.be.empty",
+ asList(Argument.arg("invalid", null),
+ Argument.arg("property", "lastName"))),
+ tuple("property.should.not.be.empty",
+ asList(Argument.arg("invalid", null),
+ Argument.arg("property", "email"))),
+ tuple("property.should.not.be.empty",
+ asList(Argument.arg("invalid", null),
+ Argument.arg("property", "password")))
+
+ );
+
}
private Object[] provideParamsForCanHandle() {
@@ -107,42 +137,41 @@ private Object[] provideParamsForCanHandle() {
private Object[] provideParamsForHandle() {
return p(
- p(tbv("ali", 0, "coding"), e("age.min"),
- singletonMap("age.min", asList(
+ p(tbv("ali", 0, "coding"),
+ errors(error("age.min", asList(
arg("value", 1L),
arg("invalid", 0),
- arg("property", "age")))),
- p(tbv("ali", 29), e("interests.limit"),
- singletonMap("interests.limit", asList(
+ arg("property", "age"))))),
+ p(tbv("ali", 29),
+ errors(error("interests.limit", asList(
arg("max", 6),
arg("min", 1),
arg("invalid", emptyList()),
- arg("property", "interests")))),
- p(tbv("", 29, "coding"), e("name.required"),
- singletonMap("name.required", asList(
+ arg("property", "interests"))))),
+ p(tbv("", 29, "coding"),
+ errors(error("name.required", asList(
arg("invalid", ""),
- arg("property", "name")))),
- p(tbv("", 200), e("name.required", "age.max", "interests.limit"),
- m(
- "age.max", asList(
+ arg("property", "name"))))),
+ p(tbv("", 200),
+ errors(
+ error("age.max", asList(
arg("value", 100L),
arg("invalid", 200),
- arg("property", "age")),
- "interests.limit", asList(
+ arg("property", "age"))),
+ error("interests.limit", asList(
arg("max", 6),
arg("min", 1),
arg("invalid", emptyList()),
- arg("property", "interests")),
- "name.required", asList(
+ arg("property", "interests"))),
+ error("name.required", asList(
arg("invalid", ""),
- arg("property", "name"))
+ arg("property", "name")))
)
),
p(tbv("ali", 29, singletonList("coding"), asList(tbvChild("given"), tbvChild(""), tbvChild("also given"))),
- e("stringField.required"),
- singletonMap("stringField.required", asList(
+ errors(error("stringField.required", asList(
arg("invalid", ""),
- arg("property", "tbvChildren[1].stringField"))))
+ arg("property", "tbvChildren[1].stringField")))))
);
}
@@ -150,6 +179,14 @@ private Set e(String... errorCodes) {
return new HashSet<>(asList(errorCodes));
}
+ private List errors(ErrorWithArguments... errors) {
+ return Arrays.asList(errors);
+ }
+
+ private ErrorWithArguments error(String errorCode, List arguments) {
+ return new ErrorWithArguments(errorCode, arguments);
+ }
+
private Map m(String k1, List