diff --git a/pom.xml b/pom.xml index 37a6b6a..24353a4 100644 --- a/pom.xml +++ b/pom.xml @@ -83,11 +83,6 @@ commons-collections4 4.4 - - org.apache.httpcomponents.client5 - httpclient5 - 5.2.1 - diff --git a/src/main/java/zw/qantra/tm/exceptions/ApiException.java b/src/main/java/zw/qantra/tm/exceptions/ApiException.java new file mode 100755 index 0000000..8f11093 --- /dev/null +++ b/src/main/java/zw/qantra/tm/exceptions/ApiException.java @@ -0,0 +1,15 @@ +package zw.qantra.tm.exceptions; + +public class ApiException extends RuntimeException { + + private String message; + + public ApiException(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/zw/qantra/tm/exceptions/GeneralExceptionHandler.java b/src/main/java/zw/qantra/tm/exceptions/GeneralExceptionHandler.java new file mode 100755 index 0000000..d5380d2 --- /dev/null +++ b/src/main/java/zw/qantra/tm/exceptions/GeneralExceptionHandler.java @@ -0,0 +1,99 @@ +package zw.qantra.tm.exceptions; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import zw.qantra.tm.rest.ApiResponse; + +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * * Handle all exceptions and java bean validation errors + * for all endpoints' income data that use the @Valid annotation + * + * @author Ehab Qadah + */ +@ControllerAdvice +public class GeneralExceptionHandler extends ResponseEntityExceptionHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass().getName()); + + public static final String ACCESS_DENIED = "Access denied!"; + public static final String INVALID_REQUEST = "Invalid request"; + public static final String ERROR_MESSAGE_TEMPLATE = "message: %s %n requested uri: %s"; + public static final String LIST_JOIN_DELIMITER = ","; + public static final String FIELD_ERROR_SEPARATOR = ": "; + private static final String ERRORS_FOR_PATH = "errors {} for path {}"; + private static final String PATH = "path"; + private static final String ERRORS = "error"; + private static final String STATUS = "status"; + private static final String MESSAGE = "message"; + private static final String TIMESTAMP = "timestamp"; + private static final String TYPE = "type"; + private static final String BODY = "body"; + + /** + * A general handler for all uncaught exceptions + */ + @ExceptionHandler({Exception.class}) + public ResponseEntity handleAllExceptions(Exception exception, WebRequest request) { + exception.printStackTrace(); + ResponseStatus responseStatus = + exception.getClass().getAnnotation(ResponseStatus.class); + final HttpStatus status = + responseStatus!=null ? responseStatus.value():HttpStatus.INTERNAL_SERVER_ERROR; + final String localizedMessage = exception.getLocalizedMessage(); + final String path = request.getDescription(false); + String message = (StringUtils.isNotEmpty(localizedMessage) ? localizedMessage:status.getReasonPhrase()); + + return getExceptionResponseEntity(exception, status, request, Collections.singletonList(message)); + } + + /** + * Build detailed information about the exception in the response + */ + private ResponseEntity getExceptionResponseEntity(final Exception exception, + final HttpStatus status, + final WebRequest request, + final List errors) { + final Map body = new LinkedHashMap<>(); + final String path = request.getDescription(false); + body.put(TIMESTAMP, Instant.now()); + body.put(STATUS, status.value()); + body.put(ERRORS, errors); + body.put(TYPE, exception.getClass().getSimpleName()); + body.put(PATH, path); + body.put(BODY, request.getAttribute("mobileNumber", RequestAttributes.SCOPE_REQUEST)); + body.put(MESSAGE, getMessageForStatus(status)); + final String errorsMessage = CollectionUtils.isNotEmpty(errors) ? + errors.stream().filter(StringUtils::isNotEmpty).collect(Collectors.joining(LIST_JOIN_DELIMITER)) + :status.getReasonPhrase(); + logger.info(ERRORS_FOR_PATH, errorsMessage, path); + return ApiResponse.build(status, getMessageForStatus(status), null, errors, false); + } + + private String getMessageForStatus(HttpStatus status) { + switch (status) { + case UNAUTHORIZED: + return ACCESS_DENIED; + case BAD_REQUEST: + return INVALID_REQUEST; + default: + return status.getReasonPhrase(); + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/exceptions/RestException.java b/src/main/java/zw/qantra/tm/exceptions/RestException.java new file mode 100755 index 0000000..b0a38a0 --- /dev/null +++ b/src/main/java/zw/qantra/tm/exceptions/RestException.java @@ -0,0 +1,25 @@ +package zw.qantra.tm.exceptions; + +public class RestException extends RuntimeException { + + private String message; + private Object responsePayload; + + public RestException(Object responsePayload, String message) { + this.responsePayload = responsePayload; + this.message = message; + } + + public Object getResponsePayload() { + return responsePayload; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java b/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java new file mode 100755 index 0000000..e34e715 --- /dev/null +++ b/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java @@ -0,0 +1,46 @@ +package zw.qantra.tm.exceptions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResponseErrorHandler; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +@Component +public class RestTemplateResponseErrorHandler + implements ResponseErrorHandler { + private Logger logger = LoggerFactory.getLogger(this.getClass().toString()); + + @Override + public boolean hasError(ClientHttpResponse httpResponse) + throws IOException { + return ( + httpResponse.getStatusCode().is5xxServerError() + || httpResponse.getStatusCode().is4xxClientError()); + } + + @Override + public void handleError(ClientHttpResponse httpResponse) + throws IOException { + + logger.info("Rest error code: " + httpResponse.getStatusCode()); + + if (httpResponse.getStatusCode() + .is5xxServerError()) { + // handle SERVER_ERROR + logger.info("Server Error"); + } else if (httpResponse.getStatusCode().is4xxClientError()) { + // handle CLIENT_ERROR + logger.info("Client Error"); + BufferedReader reader = new BufferedReader(new InputStreamReader(httpResponse.getBody())); + String httpBodyResponse = reader.lines().collect(Collectors.joining("")); + + throw new RestException(httpBodyResponse, "Client Error"); + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/rest/ApiResponse.java b/src/main/java/zw/qantra/tm/rest/ApiResponse.java new file mode 100755 index 0000000..03cbf76 --- /dev/null +++ b/src/main/java/zw/qantra/tm/rest/ApiResponse.java @@ -0,0 +1,80 @@ +package zw.qantra.tm.rest; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.HashMap; +import java.util.Map; + +@Setter +@Getter +public class ApiResponse { + + private String message; + private HashMap response; + private Boolean success; + private HashMap body; + + private Map errors = new HashMap<>(); + + // private Object body; + + private HttpStatus status = HttpStatus.OK; + + public ApiResponse(String message) { + super(); + this.message = message; + } + + + public ResponseEntity ApiResponse(Object obj, HttpStatus status) { + return new ResponseEntity(obj, status); + } + + public static ResponseEntity ok(Object obj){ + return ApiResponse.build(HttpStatus.OK, "SUCCESSFUL", obj, null, true); + } + + public static ResponseEntity created(Object obj){ + return ApiResponse.build(HttpStatus.CREATED, "CREATED", obj, null, true); + } + + public static ResponseEntity badRequest(Object obj){ + return ApiResponse.build(HttpStatus.BAD_REQUEST, "BAD REQUEST", null, obj, false); + } + + public static ResponseEntity notFound(){ + return ApiResponse.build(HttpStatus.NOT_FOUND, "NOT FOUND", null, null, false); + } + + public static ResponseEntity notContent(){ + return ApiResponse.build(HttpStatus.NO_CONTENT, "DELETED", null, null, false); + } + + public static ResponseEntity badRequest(Map errors){ + return ApiResponse.build(HttpStatus.BAD_REQUEST, "Problem with request", null, errors, false); + } + + public static ResponseEntity build(HttpStatus status, String message, Object body, Object errors, Boolean success){ + Map map = new HashMap<>(); + map.put("message", message); + map.put("body", body); + map.put("errors", errors); + map.put("success", success); + map.put("status", status.value()); + + return new ResponseEntity(map, status); + } + + public ResponseEntity build(){ + Map map = new HashMap<>(); + map.put("message", this.message); + map.put("body", this.body); + map.put("errors", this.errors); +// map.put("status", this.errors); + + return new ResponseEntity(map, this.status); + } +} \ No newline at end of file