diff --git a/pom.xml b/pom.xml
index cc5049eb..f344cc33 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,7 +97,7 @@
io.quarkus
quarkus-smallrye-fault-tolerance
-
+
org.hibernate
hibernate-jpamodelgen
@@ -106,7 +106,7 @@
io.quarkus
quarkus-spring-data-jpa
-
+
com.querydsl
querydsl-jpa
@@ -198,6 +198,45 @@
rest-assured
test
+
+
+ io.github.m-m-m
+ mmm-base
+ 0.2.0
+
+
+
+ net.sf.m-m-m
+ mmm-util-validation
+ 8.7.0
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+ org.hibernate.validator
+ hibernate-validator
+ test
+
+
+
+ javax.el
+ javax.el-api
+ 2.2.4
+ test
+
+
+
+ org.glassfish
+ javax.el
+ 3.0.1-b11
+ test
+
+
@@ -238,8 +277,8 @@
-
-
+
+
com.mysema.maven
apt-maven-plugin
diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java b/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java
new file mode 100644
index 00000000..ed9f7341
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/exceptionutils/ProductNotFoundException.java
@@ -0,0 +1,17 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import net.sf.mmm.util.exception.api.NlsRuntimeException;
+
+public class ProductNotFoundException extends NlsRuntimeException {
+
+ private final String message;
+
+ /**
+ * The constructor.
+ */
+ public ProductNotFoundException(String message) {
+
+ this.message = message;
+ }
+
+}
diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java b/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java
new file mode 100644
index 00000000..39f45a11
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacade.java
@@ -0,0 +1,460 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Path.Node;
+import javax.validation.ValidationException;
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.ServerErrorException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import net.sf.mmm.util.exception.api.NlsRuntimeException;
+import net.sf.mmm.util.exception.api.NlsThrowable;
+import net.sf.mmm.util.exception.api.TechnicalErrorUserException;
+import net.sf.mmm.util.exception.api.ValidationErrorUserException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * This is an implementation of {@link ExceptionMapper} that acts as generic exception facade for REST services. It
+ * {@link #toResponse(Throwable) maps} {@link Throwable exceptions} to an according HTTP status code and JSON result as
+ * defined by
+ * devonfw
+ * REST error specification.
+ *
+ * @since 2.0.0
+ */
+@Provider
+@Priority(value = 1)
+public class RestServiceExceptionFacade implements ExceptionMapper {
+
+ /** Logger instance. */
+ private static final Logger LOG = LoggerFactory.getLogger(RestServiceExceptionFacade.class);
+
+ private final Class extends Throwable> rollbackException;
+
+ private ObjectMapper mapper;
+
+ private boolean exposeInternalErrorDetails;
+
+ /**
+ * The constructor.
+ */
+ public RestServiceExceptionFacade() {
+
+ super();
+ this.rollbackException = loadException("javax.persistence.RollbackException");
+ }
+
+ private Class extends Throwable> loadException(String className) {
+
+ try {
+ @SuppressWarnings("unchecked")
+ Class extends Throwable> exception = (Class extends Throwable>) Class.forName(className);
+ return exception;
+ } catch (ClassNotFoundException e) {
+ LOG.info("Exception {} was not found on classpath and can not be handled by this {}.", className,
+ getClass().getSimpleName());
+ } catch (Exception e) {
+ LOG.error("Exception {} is invalid and can not be handled by this {}.", className, getClass().getSimpleName(), e);
+ }
+ return null;
+ }
+
+ @Override
+ public Response toResponse(Throwable exception) {
+
+ if (exception instanceof WebApplicationException) {
+ return createResponse((WebApplicationException) exception);
+ } else if (exception instanceof NlsRuntimeException) {
+ return toResponse(exception, exception);
+ } else {
+ Throwable error = exception;
+ Throwable catched = exception;
+ error = getRollbackCause(exception);
+ if (error == null) {
+ error = unwrapNlsUserError(exception);
+ }
+ if (error == null) {
+ error = exception;
+ }
+ return toResponse(error, catched);
+ }
+ }
+
+ /**
+ * Unwraps potential NLS user error from a wrapper exception such as {@code JsonMappingException} or
+ * {@code PersistenceException}.
+ *
+ * @param exception the exception to unwrap.
+ * @return the unwrapped {@link NlsRuntimeException} exception or {@code null} if no
+ * {@link NlsRuntimeException#isForUser() use error}.
+ */
+ private NlsRuntimeException unwrapNlsUserError(Throwable exception) {
+
+ Throwable cause = exception.getCause();
+ if (cause instanceof NlsRuntimeException) {
+ NlsRuntimeException nlsError = (NlsRuntimeException) cause;
+ if (nlsError.isForUser()) {
+ return nlsError;
+ }
+ }
+ return null;
+ }
+
+ private Throwable getRollbackCause(Throwable exception) {
+
+ Class> exceptionClass = exception.getClass();
+ // if (exceptionClass == this.transactionSystemException) {
+ Throwable cause = exception.getCause();
+ if (cause != null) {
+ exceptionClass = cause.getClass();
+ if (exceptionClass == this.rollbackException) {
+ return cause.getCause();
+ }
+ }
+ // }
+ return null;
+ }
+
+ /**
+ * @see #toResponse(Throwable)
+ *
+ * @param exception the exception to handle
+ * @param catched the original exception that was cached. Either same as {@code error} or a (child-)
+ * {@link Throwable#getCause() cause} of it.
+ * @return the response build from the exception.
+ */
+ protected Response toResponse(Throwable exception, Throwable catched) {
+
+ if (exception instanceof ValidationException) {
+ return handleValidationException(exception, catched);
+ } else if (exception instanceof ValidationErrorUserException) {
+ return createResponse(exception, (ValidationErrorUserException) exception, null);
+ } else {
+ return handleGenericError(exception, catched);
+ }
+ }
+
+ /**
+ * Creates the {@link Response} for the given validation exception.
+ *
+ * @param exception is the original validation exception.
+ * @param error is the wrapped exception or the same as exception.
+ * @param errorsMap is a map with all validation errors
+ * @return the requested {@link Response}.
+ */
+ protected Response createResponse(Throwable exception, ValidationErrorUserException error,
+ Map> errorsMap) {
+
+ LOG.warn("Service failed due to validation failure.", error);
+ if (exception == error) {
+ return createResponse(Status.BAD_REQUEST, error, errorsMap);
+ } else {
+ return createResponse(Status.BAD_REQUEST, error, exception.getMessage(), errorsMap);
+ }
+ }
+
+ /**
+ * Exception handling for generic exception (fallback).
+ *
+ * @param exception the exception to handle
+ * @param catched the original exception that was cached. Either same as {@code error} or a (child-)
+ * {@link Throwable#getCause() cause} of it.z
+ * @return the response build from the exception
+ */
+ protected Response handleGenericError(Throwable exception, Throwable catched) {
+
+ NlsRuntimeException userError;
+ boolean logged = false;
+ if (exception instanceof NlsThrowable) {
+ NlsThrowable nlsError = (NlsThrowable) exception;
+ if (!nlsError.isTechnical()) {
+ LOG.warn("Service failed due to business error: {}", nlsError.getMessage());
+ logged = true;
+ }
+ userError = TechnicalErrorUserException.getOrCreateUserException(exception);
+ } else {
+ userError = TechnicalErrorUserException.getOrCreateUserException(catched);
+ }
+ if (!logged) {
+ LOG.error("Service failed on server", userError);
+ }
+ return createResponse(userError);
+ }
+
+ /**
+ * Exception handling for validation exception.
+ *
+ * @param exception the exception to handle
+ * @param catched the original exception that was cached. Either same as {@code error} or a (child-)
+ * {@link Throwable#getCause() cause} of it.
+ * @return the response build from the exception.
+ */
+ protected Response handleValidationException(Throwable exception, Throwable catched) {
+
+ Throwable t = catched;
+ Map> errorsMap = null;
+ if (exception instanceof ConstraintViolationException) {
+ ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception;
+ Set> violations = constraintViolationException.getConstraintViolations();
+ errorsMap = new HashMap<>();
+
+ for (ConstraintViolation> violation : violations) {
+ Iterator it = violation.getPropertyPath().iterator();
+ String fieldName = null;
+
+ // Getting fieldname from the exception
+ while (it.hasNext()) {
+ fieldName = it.next().toString();
+ }
+
+ List errorsList = errorsMap.get(fieldName);
+
+ if (errorsList == null) {
+ errorsList = new ArrayList<>();
+ errorsMap.put(fieldName, errorsList);
+ }
+
+ errorsList.add(violation.getMessage());
+
+ }
+
+ t = new ValidationException(errorsMap.toString(), catched);
+ }
+ ValidationErrorUserException error = new ValidationErrorUserException(t);
+ return createResponse(t, error, errorsMap);
+ }
+
+ /**
+ * @param error is the {@link Throwable} to extract message details from.
+ * @return the exposed message(s).
+ */
+ protected String getExposedErrorDetails(Throwable error) {
+
+ StringBuilder buffer = new StringBuilder();
+ Throwable e = error;
+ while (e != null) {
+ if (buffer.length() > 0) {
+ buffer.append(System.lineSeparator());
+ }
+ buffer.append(e.getClass().getSimpleName());
+ buffer.append(": ");
+ buffer.append(e.getMessage());
+ e = e.getCause();
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Create the {@link Response} for the given {@link NlsRuntimeException}.
+ *
+ * @param error the generic {@link NlsRuntimeException}.
+ * @return the corresponding {@link Response}.
+ */
+ protected Response createResponse(NlsRuntimeException error) {
+
+ Status status;
+ if (error.isTechnical()) {
+ status = Status.INTERNAL_SERVER_ERROR;
+ } else {
+ status = Status.BAD_REQUEST;
+ }
+ return createResponse(status, error, null);
+ }
+
+ /**
+ * Create a response message as a JSON-String from the given parts.
+ *
+ * @param status is the HTTP {@link Status}.
+ * @param error is the catched or wrapped {@link NlsRuntimeException}.
+ * @param errorsMap is a map with all validation errors
+ * @return the corresponding {@link Response}.
+ */
+ protected Response createResponse(Status status, NlsRuntimeException error, Map> errorsMap) {
+
+ String message;
+ if (this.exposeInternalErrorDetails) {
+ message = getExposedErrorDetails(error);
+ } else {
+ message = error.getMessage();
+ }
+ return createResponse(status, error, message, errorsMap);
+ }
+
+ /**
+ * Create a response message as a JSON-String from the given parts.
+ *
+ * @param status is the HTTP {@link Status}.
+ * @param error is the catched or wrapped {@link NlsRuntimeException}.
+ * @param message is the JSON message attribute.
+ * @param errorsMap is a map with all validation errors
+ * @return the corresponding {@link Response}.
+ */
+ protected Response createResponse(Status status, NlsRuntimeException error, String message,
+ Map> errorsMap) {
+
+ return createResponse(status, error, message, error.getCode(), errorsMap);
+ }
+
+ /**
+ * Create a response message as a JSON-String from the given parts.
+ *
+ * @param status is the HTTP {@link Status}.
+ * @param error is the catched or wrapped {@link NlsRuntimeException}.
+ * @param message is the JSON message attribute.
+ * @param code is the {@link NlsRuntimeException#getCode() error code}.
+ * @param errorsMap is a map with all validation errors
+ * @return the corresponding {@link Response}.
+ */
+ protected Response createResponse(Status status, NlsRuntimeException error, String message, String code,
+ Map> errorsMap) {
+
+ return createResponse(status, message, code, error.getUuid(), errorsMap);
+ }
+
+ /**
+ * Create a response message as a JSON-String from the given parts.
+ *
+ * @param status is the HTTP {@link Status}.
+ * @param message is the JSON message attribute.
+ * @param code is the {@link NlsRuntimeException#getCode() error code}.
+ * @param uuid the {@link UUID} of the response message.
+ * @param errorsMap is a map with all validation errors
+ * @return the corresponding {@link Response}.
+ */
+ protected Response createResponse(Status status, String message, String code, UUID uuid,
+ Map> errorsMap) {
+
+ String json = createJsonErrorResponseMessage(message, code, uuid, errorsMap);
+ return Response.status(status).entity(json).build();
+ }
+
+ /**
+ * Create a response message as a JSON-String from the given parts.
+ *
+ * @param message the message of the response message
+ * @param code the code of the response message
+ * @param uuid the uuid of the response message
+ * @param errorsMap is a map with all validation errors
+ * @return the response message as a JSON-String
+ */
+ protected String createJsonErrorResponseMessage(String message, String code, UUID uuid,
+ Map> errorsMap) {
+
+ Map jsonMap = new HashMap<>();
+ if (message != null) {
+ jsonMap.put(ServiceConstants.KEY_MESSAGE, message);
+ }
+ if (code != null) {
+ jsonMap.put(ServiceConstants.KEY_CODE, code);
+ }
+ if (uuid != null) {
+ jsonMap.put(ServiceConstants.KEY_UUID, uuid.toString());
+ }
+
+ if (errorsMap != null) {
+ jsonMap.put(ServiceConstants.KEY_ERRORS, errorsMap);
+ }
+
+ String responseMessage = "";
+ try {
+ responseMessage = this.mapper.writeValueAsString(jsonMap);
+ } catch (JsonProcessingException e) {
+ LOG.error("Exception facade failed to create JSON.", e);
+ responseMessage = "{}";
+ }
+ return responseMessage;
+
+ }
+
+ /**
+ * Add a response message to an existing response.
+ *
+ * @param exception the {@link WebApplicationException}.
+ * @return the response with the response message added
+ */
+ protected Response createResponse(WebApplicationException exception) {
+
+ Response response = exception.getResponse();
+ int statusCode = response.getStatus();
+ Status status = Status.fromStatusCode(statusCode);
+ NlsRuntimeException error;
+ if (exception instanceof ServerErrorException) {
+ error = new TechnicalErrorUserException(exception);
+ LOG.error("Service failed on server", error);
+ return createResponse(status, error, null);
+ } else {
+ UUID uuid = UUID.randomUUID();
+ if (exception instanceof ClientErrorException) {
+ LOG.warn("Service failed due to unexpected request. UUDI: {}, reason: {} ", uuid, exception.getMessage());
+ } else {
+ LOG.warn("Service caused redirect or other error. UUID: {}, reason: {}", uuid, exception.getMessage());
+ }
+ return createResponse(status, exception.getMessage(), String.valueOf(statusCode), uuid, null);
+ }
+
+ }
+
+ /**
+ * @return the {@link ObjectMapper} for JSON mapping.
+ */
+ public ObjectMapper getMapper() {
+
+ return this.mapper;
+ }
+
+ /**
+ * @param mapper the mapper to set
+ */
+ @Inject
+ public void setMapper(ObjectMapper mapper) {
+
+ this.mapper = mapper;
+ }
+
+ /**
+ * @param exposeInternalErrorDetails - {@code true} if internal exception details shall be exposed to clients (useful
+ * for debugging and testing), {@code false} if such details are hidden to prevent
+ * Sensitive Data Exposure
+ * (default, has to be used in production environment).
+ */
+ public void setExposeInternalErrorDetails(boolean exposeInternalErrorDetails) {
+
+ this.exposeInternalErrorDetails = exposeInternalErrorDetails;
+ if (exposeInternalErrorDetails) {
+ String message = "****** Exposing of internal error details is enabled! This violates OWASP A6 (Sensitive Data Exposure) and shall only be used for testing/debugging and never in production. ******";
+ LOG.warn(message);
+ // CHECKSTYLE:OFF (for development only)
+ System.err.println(message);
+ // CHECKSTYLE:ON
+ }
+ }
+
+ /**
+ * @return exposeInternalErrorDetails the value set by {@link #setExposeInternalErrorDetails(boolean)}.
+ */
+ public boolean isExposeInternalErrorDetails() {
+
+ return this.exposeInternalErrorDetails;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java b/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java
new file mode 100644
index 00000000..deabf02d
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/exceptionutils/ServiceConstants.java
@@ -0,0 +1,72 @@
+package com.devonfw.quarkus.exceptionutils;
+
+/**
+ * Constants for {@link com.devonfw.module.service.common.api.Service}s.
+ *
+ * @since 3.0.0
+ */
+public class ServiceConstants {
+
+ /** Key for {@link Throwable#getMessage() error message}. */
+ public static final String KEY_MESSAGE = "message";
+
+ /** Key for error {@link java.util.UUID}. */
+ public static final String KEY_UUID = "uuid";
+
+ /** Key for error code. */
+ public static final String KEY_CODE = "code";
+
+ /** Key for HTTP status code. */
+ public static final String KEY_STATUS = "status";
+
+ /** Key for HTTP error. */
+ public static final String KEY_ERROR = "error";
+
+ /** Key for (validation) error details. */
+ public static final String KEY_ERRORS = "errors";
+
+ /** The services URL folder. */
+ public static final String URL_FOLDER_SERVICES = "services";
+
+ /** The services URL path. */
+ public static final String URL_PATH_SERVICES = "/" + URL_FOLDER_SERVICES;
+
+ /** The rest URL folder. */
+ public static final String URL_FOLDER_REST = "rest";
+
+ /** The web-service URL folder. */
+ public static final String URL_FOLDER_WEB_SERVICE = "ws";
+
+ /** The rest services URL path. */
+ public static final String URL_PATH_REST_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_REST;
+
+ /** The web-service URL path. */
+ public static final String URL_PATH_WEB_SERVICES = URL_PATH_SERVICES + "/" + URL_FOLDER_WEB_SERVICE;
+
+ /**
+ * The variable that resolves to the
+ * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() technical name of the
+ * application}.
+ */
+ public static final String VARIABLE_APP = "${app}";
+
+ /**
+ * The variable that resolves to the
+ * {@link com.devonfw.module.basic.common.api.reflect.Devon4jPackage#getApplication() technical name of the
+ * application}.
+ */
+ public static final String VARIABLE_LOCAL_SERVER_PORT = "${local.server.port}";
+
+ /**
+ * The variable that resolves to type of the service (e.g. "rest" for REST service and "ws" for SOAP service).
+ */
+ public static final String VARIABLE_TYPE = "${type}";
+
+ /**
+ * URL suffix for convention to retrieve WSDL from SOAP service.
+ *
+ * @since 2021.04.003
+ */
+ public static final String WSDL_SUFFIX = "?wsdl";
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java b/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java
index 4cb9c128..49b7fc70 100644
--- a/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java
+++ b/src/main/java/com/devonfw/quarkus/productmanagement/service/v1/ProductRestService.java
@@ -22,6 +22,7 @@
import org.springframework.data.domain.Page;
import org.tkit.quarkus.rs.models.PageResultDTO;
+import com.devonfw.quarkus.exceptionutils.ProductNotFoundException;
import com.devonfw.quarkus.productmanagement.logic.UcFindProduct;
import com.devonfw.quarkus.productmanagement.logic.UcManageProduct;
import com.devonfw.quarkus.productmanagement.service.v1.model.NewProductDto;
@@ -113,9 +114,13 @@ public ProductDto createNewProduct(NewProductDto dto) {
@APIResponse(responseCode = "404", description = "Product not found"), @APIResponse(responseCode = "500") })
@Operation(operationId = "getProductById", description = "Returns Product with given id")
@GET
- @Path("{id}")
+ @Path("/id/{id}")
public ProductDto getProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {
+ // A sample scenario to simulate exception mapping
+ if (Integer.parseInt(id) > 100) {
+ throw new ProductNotFoundException("Products not found!!");
+ }
return this.ucFindProduct.findProduct(id);
}
diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java
new file mode 100644
index 00000000..bdc10d60
--- /dev/null
+++ b/src/test/java/com/devonfw/quarkus/exceptionutils/BaseTest.java
@@ -0,0 +1,64 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+public abstract class BaseTest extends Assertions {
+
+ private static boolean initialSetup = false;
+
+ /**
+ * Initializes this test class and resets {@link #isInitialSetup() initial setup flag}.
+ */
+ @BeforeClass
+ public static void setUpClass() {
+
+ initialSetup = true;
+ }
+
+ /**
+ * Suggests to use {@link #doSetUp()} method before each tests.
+ */
+ @Before
+ public final void setUp() {
+
+ doSetUp();
+ if (initialSetup) {
+ initialSetup = false;
+ }
+ }
+
+ /**
+ * Suggests to use {@link #doTearDown()} method before each tests.
+ */
+ @After
+ public final void tearDown() {
+
+ doTearDown();
+ }
+
+ /**
+ * @return {@code true} if this JUnit class is invoked for the first time (first test method is called), {@code false}
+ * otherwise (if this is a subsequent invocation).
+ */
+ protected boolean isInitialSetup() {
+
+ return initialSetup;
+ }
+
+ /**
+ * Provides initialization previous to the creation of the text fixture.
+ */
+ protected void doSetUp() {
+
+ }
+
+ /**
+ * Provides clean up after tests.
+ */
+ protected void doTearDown() {
+
+ }
+}
diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java
new file mode 100644
index 00000000..974e1108
--- /dev/null
+++ b/src/test/java/com/devonfw/quarkus/exceptionutils/CategoryModuleTest.java
@@ -0,0 +1,8 @@
+package com.devonfw.quarkus.exceptionutils;
+
+/**
+ * This is the JUnit {@link org.junit.experimental.categories.Category}
+ */
+public interface CategoryModuleTest {
+
+}
diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java
new file mode 100644
index 00000000..60b74d6c
--- /dev/null
+++ b/src/test/java/com/devonfw/quarkus/exceptionutils/ModuleTest.java
@@ -0,0 +1,16 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import org.junit.experimental.categories.Category;
+
+/**
+ * This is the abstract base class for a module test. You are free to create your module tests as you like just by
+ * annotating {@link CategoryModuleTest} using {@link Category}. However, in most cases it will be convenient just to
+ * extend this class.
+ *
+ * @see ModuleTest
+ *
+ */
+@Category(CategoryModuleTest.class)
+public abstract class ModuleTest extends BaseTest {
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java
new file mode 100644
index 00000000..5ce2b095
--- /dev/null
+++ b/src/test/java/com/devonfw/quarkus/exceptionutils/RestServiceExceptionFacadeTest.java
@@ -0,0 +1,281 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
+import javax.validation.ValidationException;
+import javax.validation.Validator;
+import javax.validation.constraints.Min;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.InternalServerErrorException;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import net.sf.mmm.util.exception.api.IllegalCaseException;
+import net.sf.mmm.util.exception.api.NlsRuntimeException;
+import net.sf.mmm.util.exception.api.ObjectNotFoundUserException;
+import net.sf.mmm.util.exception.api.TechnicalErrorUserException;
+import net.sf.mmm.util.exception.api.ValidationErrorUserException;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Test-case for {@link RestServiceExceptionFacade}.
+ *
+ */
+public class RestServiceExceptionFacadeTest extends ModuleTest {
+
+ /** Value of {@link TechnicalErrorUserException#getCode()}. */
+ private static final String CODE_TECHNICAL_ERROR = "TechnicalError";
+
+ /** Placeholder for any UUID. */
+ private static final String UUID_ANY = "";
+
+ /**
+ * @return the {@link RestServiceExceptionFacade} instance to test.
+ */
+ protected RestServiceExceptionFacade getExceptionFacade() {
+
+ RestServiceExceptionFacade facade = new RestServiceExceptionFacade();
+ facade.setMapper(new ObjectMapper());
+ return facade;
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with constraint violations
+ */
+ @Test
+ public void testConstraintViolationExceptions() {
+
+ class CounterTest {
+
+ @Min(value = 10)
+ private Integer count;
+
+ public CounterTest(Integer count) {
+
+ this.count = count;
+ }
+
+ }
+
+ CounterTest counter = new CounterTest(new Integer(1));
+
+ Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ Set> violations = validator.validate(counter);
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String message = "{count=[" + violations.iterator().next().getMessage() + "]}";
+ String errors = "{count=[" + violations.iterator().next().getMessage() + "]}";
+ Throwable error = new ConstraintViolationException(violations);
+ checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE, errors);
+
+ }
+
+ /**
+ * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given
+ * {@link Throwable}.
+ *
+ * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test.
+ * @param error is the {@link Throwable} to convert.
+ * @param statusCode is the expected {@link Response#getStatus() status} code.
+ * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result.
+ * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be {@code null}.
+ * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be
+ * {@code null}.
+ * @return the JSON result for potential further asserts.
+ */
+ protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode,
+ String message, String uuid, String code) {
+
+ return checkFacade(exceptionFacade, error, statusCode, message, uuid, code, null);
+ }
+
+ /**
+ * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given
+ * {@link Throwable}.
+ *
+ * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test.
+ * @param error is the {@link Throwable} to convert.
+ * @param statusCode is the expected {@link Response#getStatus() status} code.
+ * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result.
+ * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be {@code null}.
+ * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be
+ * {@code null}.
+ * @param errors is the expected validation errors in a format key-value
+ * @return the JSON result for potential further asserts.
+ */
+ @SuppressWarnings("unchecked")
+ protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode,
+ String message, String uuid, String code, String errors) {
+
+ Response response = exceptionFacade.toResponse(error);
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(statusCode);
+
+ Object entity = response.getEntity();
+ assertThat(entity).isInstanceOf(String.class);
+ String result = (String) entity;
+
+ try {
+ Map valueMap = exceptionFacade.getMapper().readValue(result, Map.class);
+ String msg = message;
+ if (msg == null) {
+ msg = error.getMessage();
+ }
+ assertThat(valueMap.get(ServiceConstants.KEY_MESSAGE)).isEqualTo(msg);
+ if ((statusCode == 403) && (!exceptionFacade.isExposeInternalErrorDetails())) {
+ assertThat(result).doesNotContain(error.getMessage());
+ }
+ assertThat(valueMap.get(ServiceConstants.KEY_CODE)).isEqualTo(code);
+ String actualUuid = (String) valueMap.get(ServiceConstants.KEY_UUID);
+ if (UUID_ANY.equals(uuid)) {
+ if (actualUuid == null) {
+ fail("UUID expected but not found in response: " + result);
+ }
+ } else {
+ assertThat(actualUuid).isEqualTo(uuid);
+ }
+
+ Map> errorsMap = (Map>) valueMap.get(ServiceConstants.KEY_ERRORS);
+
+ if (errors == null) {
+ if (errorsMap != null) {
+ fail("Errors do not expected but found in response: " + result);
+ } else {
+ assertThat(errorsMap).isEqualTo(errors);
+ }
+ } else {
+ if (errorsMap != null) {
+ assertThat(errorsMap.toString()).isEqualTo(errors);
+ } else {
+ fail("Errors expected but not found in response: " + result);
+ }
+
+ }
+
+ } catch (Exception e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ return result;
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testJaxrsInternalServerException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String internalMessage = "The HTTP request is invalid";
+ int statusCode = 500;
+ InternalServerErrorException error = new InternalServerErrorException(internalMessage);
+ TechnicalErrorUserException technicalErrorUserException = new TechnicalErrorUserException(error);
+ String expectedMessage = technicalErrorUserException.getMessage();
+ checkFacade(exceptionFacade, technicalErrorUserException, statusCode, expectedMessage,
+ technicalErrorUserException.getUuid().toString(), CODE_TECHNICAL_ERROR);
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception.
+ */
+ @Test
+ public void testJaxrsBadRequestException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String message = "The HTTP request is invalid";
+ Throwable error = new BadRequestException(message);
+ checkFacade(exceptionFacade, error, 400, message, UUID_ANY, "400");
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with a {@link ValidationException}.
+ */
+ @Test
+ public void testValidationException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String message = "Validation failed!";
+ Throwable error = new ValidationException(message);
+ checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE);
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testJaxrsNotFoundException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String internalMessage = "Either the service URL is wrong or the requested resource does not exist";
+ checkFacade(exceptionFacade, new NotFoundException(internalMessage), 404, internalMessage, UUID_ANY, "404");
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testTechnicalJavaRuntimeServerException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String secretMessage = "Internal server error occurred";
+ IllegalArgumentException error = new IllegalArgumentException(secretMessage);
+ TechnicalErrorUserException technicalErrorUserException = new TechnicalErrorUserException(error);
+ String expectedMessage = technicalErrorUserException.getMessage();
+ checkFacade(exceptionFacade, technicalErrorUserException, 500, expectedMessage,
+ technicalErrorUserException.getUuid().toString(), CODE_TECHNICAL_ERROR);
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testTechnicalCustomRuntimeServerException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ String message = "Internal server error occurred";
+ IllegalCaseException error = new IllegalCaseException(message);
+ String expectedMessage = new TechnicalErrorUserException(error).getMessage();
+ checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR);
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testTechnicalCustomRuntimeServerExceptionExposed() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ exceptionFacade.setExposeInternalErrorDetails(true);
+ String message = "Internal server error occurred";
+ IllegalCaseException error = new IllegalCaseException(message);
+
+ String expectedMessage = "TechnicalErrorUserException: An unexpected error has occurred! We apologize any inconvenience. Please try again later."
+ + System.lineSeparator() + error.getUuid().toString() + ":" + CODE_TECHNICAL_ERROR + System.lineSeparator()
+ + error.getClass().getSimpleName() + ": " + error.getMessage();
+ checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR);
+ }
+
+ /**
+ * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including
+ * subclasses.
+ */
+ @Test
+ public void testBusinessException() {
+
+ RestServiceExceptionFacade exceptionFacade = getExceptionFacade();
+ ObjectNotFoundUserException error = new ObjectNotFoundUserException(4711L);
+ checkFacade(exceptionFacade, error, 400, null, error.getUuid().toString(), "NotFound");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java b/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java
new file mode 100644
index 00000000..5b0c73ef
--- /dev/null
+++ b/src/test/java/com/devonfw/quarkus/exceptionutils/TagModuleTest.java
@@ -0,0 +1,19 @@
+package com.devonfw.quarkus.exceptionutils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.Tag;
+
+/**
+ * This is the meta Annotation JUnit5 {@link org.junit.jupiter.api.Tag} for a Module Test
+ *
+ */
+@Tag("module")
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface TagModuleTest {
+
+}
\ No newline at end of file