Skip to content
This repository was archived by the owner on Apr 21, 2023. It is now read-only.

Reusing the Error Codes #105

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/main/java/me/alidg/errors/ErrorWithArguments.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* For example, suppose
* we have a bean like:
* <pre>
* public class User {
*
* &#64;Size(min=1, max=7, message="interests.range_limit")
* private List&lt;String&gt; interests;
* // omitted for the sake of brevity
* }
* </pre>
* If the given interest list wasn't valid, then this object would contain
* {@code interests.range_limit} as the <code>errorCode</code> and {@code List(Argument(min, 1), Argument(max, 7))}
* as the <code>arguments</code>. Later on we can use those exposed values in our message, for example,
* the following error template:
* <pre>
* You should define between {0} and {1} interests.
* </pre>
* Would be translated to:
* <pre>
* You should define between 1 and 7 interests.
* </pre>
*/
public class ErrorWithArguments {
private final String errorCode;
private final List<Argument> 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<Argument> 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<Argument> 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);
}


}
128 changes: 82 additions & 46 deletions src/main/java/me/alidg/errors/HandledException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
* <ul>
* <li>The mapped business level error codes</li>
* <li>The mapped business level error codes and their arguments that can be used for message translation</li>
* <li>The corresponding HTTP status code</li>
* <li>A collection of arguments that can be used for message translation</li>
* </ul>
*
* @author Ali Dehghani
Expand All @@ -29,38 +27,42 @@ 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<String> errorCodes;
private final List<ErrorWithArguments> errors;

/**
* Corresponding status code for the handled exception.
*/
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:
* <pre>
* public class User {
* Initialize a handled exception with a set of error codes, a HTTP status code and an
* optional collection of arguments.
*
* &#64;Size(min=1, max=7, message="interests.range_limit")
* private List&lt;String&gt; interests;
* // omitted for the sake of brevity
* }
* </pre>
* 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:
* <pre>
* You should define between {0} and {1} interests.
* </pre>
* Would be translated to:
* <pre>
* You should define between 1 and 7 interests.
* </pre>
* @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<String, List<Argument>> arguments;
public HandledException(@NonNull List<ErrorWithArguments> 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
Expand All @@ -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<String> errorCodes,
@NonNull HttpStatus statusCode,
@Nullable Map<String, List<Argument>> arguments) {
enforcePreconditions(errorCodes, statusCode);
this.errorCodes = errorCodes;
this.statusCode = statusCode;
this.arguments = arguments == null ? Collections.emptyMap() : arguments;
this(convertToErrors(errorCodes, arguments), statusCode);
}

/**
Expand All @@ -90,48 +92,82 @@ public HandledException(@NonNull Set<String> 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<String, List<Argument>> arguments) {
this(singleton(errorCode), statusCode, arguments);
}

/**
* @return Collection of mapped error codes.
* @see #errorCodes
*
* @return Collection of errors
*/
@NonNull
public Set<String> getErrorCodes() {
return errorCodes;
public List<ErrorWithArguments> 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<String> 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<String, List<Argument>> getArguments() {
return arguments;
return errors.stream()
.collect(Collectors.toMap(ErrorWithArguments::getErrorCode,
ErrorWithArguments::getArguments));
}

private void enforcePreconditions(Set<String> errorCodes, HttpStatus statusCode) {
/**
* @return The mapped status code.
* @see #statusCode
*/
@NonNull
public HttpStatus getStatusCode() {
return statusCode;
}

private void enforcePreconditions(List<ErrorWithArguments> 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<ErrorWithArguments> convertToErrors(Set<String> errorCodes, Map<String, List<Argument>> arguments) {
List<ErrorWithArguments> result = new ArrayList<>();
for (String errorCode : errorCodes) {
result.add(new ErrorWithArguments(errorCode, arguments.get(errorCode)));
}

return result;
}

}
6 changes: 4 additions & 2 deletions src/main/java/me/alidg/errors/WebErrorHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,9 @@ private Throwable refineIfNeeded(Throwable exception) {

private List<CodedMessage> 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());
}

Expand Down Expand Up @@ -265,7 +265,9 @@ private String className(Object toInspect) {
return toInspect.getClass().getName();
}

/*
private List<Argument> getArgumentsFor(HandledException handled, String errorCode) {
return handled.getArguments().getOrDefault(errorCode, emptyList());
}
*/
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -72,7 +72,7 @@ public HandledException handle(Throwable exception) {
HttpStatus httpStatus = exceptionMapping.statusCode();
List<Argument> arguments = getExposedValues(exception);

return new HandledException(errorCode, httpStatus, singletonMap(errorCode, arguments));
return new HandledException(new ErrorWithArguments(errorCode, arguments), httpStatus);
}

/**
Expand Down
Loading