From 65785ed2600d936c98f944ffc627c1fc30f2eed1 Mon Sep 17 00:00:00 2001 From: Lars Johansson <lars.johansson@ess.eu> Date: Thu, 24 Feb 2022 12:39:36 +0100 Subject: [PATCH] Added global exception handler --- .../GlobalControllerExceptionHandler.java | 66 +++++ .../openepics/names/util/ExceptionUtil.java | 255 ++++++++++++++++++ .../util/ServiceHttpStatusException.java | 80 ++++++ .../names/util/response/Response.java | 70 +++++ .../names/util/ExceptionUtilTest.java | 145 ++++++++++ 5 files changed, 616 insertions(+) create mode 100644 src/main/java/org/openepics/names/rest/controller/GlobalControllerExceptionHandler.java create mode 100644 src/main/java/org/openepics/names/util/ExceptionUtil.java create mode 100644 src/main/java/org/openepics/names/util/ServiceHttpStatusException.java create mode 100644 src/main/java/org/openepics/names/util/response/Response.java create mode 100644 src/test/java/org/openepics/names/util/ExceptionUtilTest.java diff --git a/src/main/java/org/openepics/names/rest/controller/GlobalControllerExceptionHandler.java b/src/main/java/org/openepics/names/rest/controller/GlobalControllerExceptionHandler.java new file mode 100644 index 0000000..bad2608 --- /dev/null +++ b/src/main/java/org/openepics/names/rest/controller/GlobalControllerExceptionHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.rest.controller; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.openepics.names.util.ExceptionUtil; +import org.openepics.names.util.ServiceHttpStatusException; +import org.openepics.names.util.response.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +/** + * Global exception handler to ensure exceptions are communicated to request origin in a uniform way. + * + * @author Lars Johansson + */ +@RestControllerAdvice +public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler { + + // note + // no logging here + + private static final Logger LOGGER = Logger.getLogger(GlobalControllerExceptionHandler.class.getName()); + + @ExceptionHandler + protected ResponseEntity<Response> handleConflict(RuntimeException ex, WebRequest request) { + LOGGER.log(Level.INFO, "handleConflict, ex.getMessage: " + ex.getMessage()); + + Response response = new Response("", ""); + response.setMessage(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED); + + HttpStatus resultStatus = HttpStatus.INTERNAL_SERVER_ERROR; + + if (ex instanceof ServiceHttpStatusException) { + response.setMessage(StringUtils.trimToEmpty(ex.getMessage())); + response.setDetails(StringUtils.trimToEmpty(((ServiceHttpStatusException) ex).getDetails())); + resultStatus = ((ServiceHttpStatusException) ex).getHttpStatus(); + } + + return new ResponseEntity<>(response, Response.HEADER_JSON, resultStatus); + } + +} diff --git a/src/main/java/org/openepics/names/util/ExceptionUtil.java b/src/main/java/org/openepics/names/util/ExceptionUtil.java new file mode 100644 index 0000000..e008c92 --- /dev/null +++ b/src/main/java/org/openepics/names/util/ExceptionUtil.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.util; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +/** + * Utility class to assist in handling of exceptions. + * <br/><br/> + * Note + * <ul> + * <li>400 {@link HttpStatus#BAD_REQUEST}</li> + * <li>401 {@link HttpStatus#UNAUTHORIZED}</li> + * <li>403 {@link HttpStatus#FORBIDDEN}</li> + * <li>404 {@link HttpStatus#NOT_FOUND}</li> + * <li>409 {@link HttpStatus#CONFLICT}</li> + * <li>500 {@link HttpStatus#INTERNAL_SERVER_ERROR}</li> + * <li>501 {@link HttpStatus#NOT_IMPLEMENTED}</li> + * </ul> + * + * @author Lars Johansson + */ +public class ExceptionUtil { + + public static final String ONE_OR_MORE_ELEMENTS_ARE_NOT_CORRECT = "One or more elements are not correct."; + public static final String OPERATION_COULD_NOT_BE_PERFORMED = "Operation could not be performed."; + + /** + * This class is not to be instantiated. + */ + private ExceptionUtil() { + throw new IllegalStateException("Utility class"); + } + + /** + * Convert service http status exception to response status exception. + * + * @param e service http status exception + * @return response status exception + */ + public static ResponseStatusException convertException(ServiceHttpStatusException e) { + switch (e.getHttpStatus()) { + case BAD_REQUEST: + throw ExceptionUtil.createResponseStatusExceptionBadRequest(); + case UNAUTHORIZED: + throw ExceptionUtil.createResponseStatusExceptionUnauthorized(); + case FORBIDDEN: + throw ExceptionUtil.createResponseStatusExceptionForbidden(); + case NOT_FOUND: + throw ExceptionUtil.createResponseStatusExceptionNotFound(); + case CONFLICT: + throw ExceptionUtil.createResponseStatusExceptionConflict(); + case INTERNAL_SERVER_ERROR: + throw ExceptionUtil.createResponseStatusExceptionInternalServerError(); + case NOT_IMPLEMENTED: + throw ExceptionUtil.createResponseStatusExceptionNotImplemented(); + default: + throw ExceptionUtil.createResponseStatusExceptionInternalServerError(); + } + } + + /** + * Create response status exception. + * Intended for communication from server to client. + * + * @param status http status + * @param reason reason + * @param cause cause + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusException(HttpStatus status, String reason, Throwable cause) { + return new ResponseStatusException(status, reason, cause); + } + + /** + * Create response status exception for {@link HttpStatus#BAD_REQUEST}. + * Intended for communication from server to client. + * + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusExceptionBadRequest() { + return createResponseStatusException(HttpStatus.BAD_REQUEST, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create response status exception for {@link HttpStatus#UNAUTHORIZED}. + * Intended for communication from server to client. + * + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusExceptionUnauthorized() { + return createResponseStatusException(HttpStatus.UNAUTHORIZED, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create response status exception for {@link HttpStatus#FORBIDDEN}. + * Intended for communication from server to client. + * + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusExceptionForbidden() { + return createResponseStatusException(HttpStatus.FORBIDDEN, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create response status exception for {@link HttpStatus#NOT_FOUND}. + * Intended for communication from server to client. + * + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusExceptionNotFound() { + return createResponseStatusException(HttpStatus.NOT_FOUND, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create response status exception for {@link HttpStatus#CONFLICT}. + * Intended for communication from server to client. + * + * @return response status exception + */ + protected static ResponseStatusException createResponseStatusExceptionConflict() { + return createResponseStatusException(HttpStatus.CONFLICT, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + + /** + * Create response status exception for {@link HttpStatus#INTERNAL_SERVER_ERROR}. + * Intended for communication from server to client. + * + * @return response status exception + */ + public static ResponseStatusException createResponseStatusExceptionInternalServerError() { + return createResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create response status exception for {@link HttpStatus#NOT_IMPLEMENTED}. + * Intended for communication from server to client. + * + * @return response status exception + */ + public static ResponseStatusException createResponseStatusExceptionNotImplemented() { + return createResponseStatusException(HttpStatus.NOT_IMPLEMENTED, OPERATION_COULD_NOT_BE_PERFORMED, null); + } + + /** + * Create service http status exception. + * Intended for communication inside server. + * + * @param status http status + * @param message message + * @param details details + * @param userFriendly user friendly + * @param cause cause + * @return service http status exception + */ + protected static ServiceHttpStatusException createServiceHttpStatusException(HttpStatus status, String message, String details, String userFriendly, Throwable cause) { + return new ServiceHttpStatusException(status, message, details, userFriendly, cause); + } + + /** + * Create service http status exception for {@link HttpStatus#BAD_REQUEST}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionBadRequest(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.BAD_REQUEST, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#UNAUTHORIZED}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionUnauthorized(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.UNAUTHORIZED, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#FORBIDDEN}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionForbidden(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.FORBIDDEN, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#NOT_FOUND}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionNotFound(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.NOT_FOUND, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#CONFLICT}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionConflict(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.CONFLICT, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#INTERNAL_SERVER_ERROR}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionInternalServerError(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, message, details, userFriendly, null); + } + + /** + * Create service http status exception for {@link HttpStatus#NOT_IMPLEMENTED}. + * Intended for communication inside server. + * + * @param message message + * @return service http status exception + */ + public static ServiceHttpStatusException createServiceHttpStatusExceptionNotImplemented(String message, String details, String userFriendly) { + return createServiceHttpStatusException(HttpStatus.NOT_IMPLEMENTED, message, details, userFriendly, null); + } + +} diff --git a/src/main/java/org/openepics/names/util/ServiceHttpStatusException.java b/src/main/java/org/openepics/names/util/ServiceHttpStatusException.java new file mode 100644 index 0000000..f0b79ab --- /dev/null +++ b/src/main/java/org/openepics/names/util/ServiceHttpStatusException.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.util; + +import org.springframework.http.HttpStatus; + +/** + * Exception class to assist in handling of service layer exceptions. + * + * @author Lars Johansson + */ +public class ServiceHttpStatusException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 5346696855950320329L; + + private final HttpStatus httpStatus; + private final String details; + private final String userFriendly; + + /** + * Public constructor. + * + * @param httpStatus http status + * @param message message + * @param cause cause + */ + public ServiceHttpStatusException(HttpStatus httpStatus, String message, String details, String userFriendly, Throwable cause) { + super(message, cause); + this.httpStatus = httpStatus; + this.details = details; + this.userFriendly = userFriendly; + } + + /** + * Return http status of exception. + * + * @return http status + */ + public HttpStatus getHttpStatus() { + return httpStatus; + } + + /** + * Returns details of exception. + * + * @return details + */ + public String getDetails() { + return details; + } + + /** + * Return user friendly of exception. + * + * @return user friendly + */ + public String getUserFriendly() { + return userFriendly; + } + +} diff --git a/src/main/java/org/openepics/names/util/response/Response.java b/src/main/java/org/openepics/names/util/response/Response.java new file mode 100644 index 0000000..491bb95 --- /dev/null +++ b/src/main/java/org/openepics/names/util/response/Response.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.util.response; + +import org.springframework.http.HttpHeaders; + +/** + * This class is used to ensure response to request origin are handled in a uniform way. + * + * @author Lars Johansson + */ +public class Response { + + private static final String CONTENT_TYPE = "Content-Type"; + private static final String APP_JSON = "application/json"; + + public static final HttpHeaders HEADER_JSON = new HttpHeaders(); + + private String message = null; + private String details = null; + + public Response() { + HEADER_JSON.add(Response.CONTENT_TYPE, Response.APP_JSON); + } + + public Response(String message) { + this(); + + this.message = message; + this.details = ""; + } + + public Response(String message, String details) { + this(); + + this.message = message; + this.details = details; + } + + public String getMessage() { + return this.message; + } + public void setMessage(String message) { + this.message = message; + } + + public String getDetails() { + return this.details; + } + public void setDetails(String details) { + this.details = details; + } + +} diff --git a/src/test/java/org/openepics/names/util/ExceptionUtilTest.java b/src/test/java/org/openepics/names/util/ExceptionUtilTest.java new file mode 100644 index 0000000..af5aae8 --- /dev/null +++ b/src/test/java/org/openepics/names/util/ExceptionUtilTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +/** + * Unit tests for ExceptionUtil class. + * + * @author Lars Johansson + * + * @see ExceptionUtil + */ +public class ExceptionUtilTest { + + /** + * Test of create response status exception. + */ + @Test + public void createResponseStatusException() { + ResponseStatusException exception = ExceptionUtil.createResponseStatusException(HttpStatus.I_AM_A_TEAPOT, null, null); + assertEquals(HttpStatus.I_AM_A_TEAPOT, exception.getStatus()); + assertNull (exception.getReason()); + assertNull (exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionBadRequest(); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionUnauthorized(); + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionForbidden(); + assertEquals(HttpStatus.FORBIDDEN, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionNotFound(); + assertEquals(HttpStatus.NOT_FOUND, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionConflict(); + assertEquals(HttpStatus.CONFLICT, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionInternalServerError(); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createResponseStatusExceptionNotImplemented(); + assertEquals(HttpStatus.NOT_IMPLEMENTED, exception.getStatus()); + assertEquals(ExceptionUtil.OPERATION_COULD_NOT_BE_PERFORMED, exception.getReason()); + assertEquals(null, exception.getCause()); + } + + /** + * Test of create service http status exception. + */ + @Test + public void createServiceHttpStatusException() { + ServiceHttpStatusException exception = ExceptionUtil.createServiceHttpStatusException(null, null, null, null, null); + assertNull(exception.getHttpStatus()); + assertNull(exception.getMessage()); + assertNull(exception.getDetails()); + assertNull(exception.getUserFriendly()); + assertNull(exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionBadRequest(null, null, null); + assertEquals(HttpStatus.BAD_REQUEST, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionUnauthorized(null, null, null); + assertEquals(HttpStatus.UNAUTHORIZED, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionForbidden(null, null, null); + assertEquals(HttpStatus.FORBIDDEN, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionNotFound(null, null, null); + assertEquals(HttpStatus.NOT_FOUND, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionConflict(null, null, null); + assertEquals(HttpStatus.CONFLICT, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionInternalServerError(null, null, null); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + + exception = ExceptionUtil.createServiceHttpStatusExceptionNotImplemented(null, null, null); + assertEquals(HttpStatus.NOT_IMPLEMENTED, exception.getHttpStatus()); + assertEquals(null, exception.getMessage()); + assertEquals(null, exception.getDetails()); + assertEquals(null, exception.getUserFriendly()); + assertEquals(null, exception.getCause()); + } + +} -- GitLab