diff --git a/pom.xml b/pom.xml
index 209d9e14..dae89d75 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,11 @@
quarkus-hibernate-orm
+
+ io.quarkus
+ quarkus-hibernate-validator
+
+
io.quarkus
@@ -103,7 +108,7 @@
provided
4.3.1
-
+
io.quarkus
quarkus-logging-json
@@ -136,8 +141,12 @@
0.2.0
provided
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
-
@@ -165,7 +174,7 @@
org.testcontainers
postgresql
test
-
+
@@ -225,7 +234,6 @@
-
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/ApplicationBusinessException.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/ApplicationBusinessException.java
new file mode 100644
index 00000000..9040b69a
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/ApplicationBusinessException.java
@@ -0,0 +1,34 @@
+package com.devonfw.quarkus.general.rest.exception;
+
+public abstract class ApplicationBusinessException extends RuntimeException {
+
+ public ApplicationBusinessException() {
+
+ super();
+ }
+
+ public ApplicationBusinessException(String message) {
+
+ super(message);
+ }
+
+ public ApplicationBusinessException(String message, Exception e) {
+
+ super(message, e);
+ }
+
+ public boolean isTechnical() {
+
+ return false;
+ }
+
+ public String getCode() {
+
+ return getClass().getSimpleName();
+ }
+
+ public Integer getStatusCode() {
+
+ return null;
+ }
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/InvalidParameterException.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/InvalidParameterException.java
new file mode 100644
index 00000000..76d9fd9e
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/InvalidParameterException.java
@@ -0,0 +1,20 @@
+package com.devonfw.quarkus.general.rest.exception;
+
+public class InvalidParameterException extends ApplicationBusinessException {
+
+ public InvalidParameterException() {
+
+ super();
+ }
+
+ public InvalidParameterException(String message) {
+
+ super(message);
+ }
+
+ @Override
+ public Integer getStatusCode() {
+
+ return Integer.valueOf(422);
+ }
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/AbstractExceptionMapper.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/AbstractExceptionMapper.java
new file mode 100644
index 00000000..b98b33e5
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/AbstractExceptionMapper.java
@@ -0,0 +1,69 @@
+package com.devonfw.quarkus.general.rest.exception.mapper;
+
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Abstract super class for all specific exception mapper. To override the default ExceptionMapper of RESTEasy, own
+ * ExceptionMapper for specific exceptions (e.g. NotFoundException) have to be created. Just using
+ * ExcecptionMapper will not work, because the RESTEasy mappers are then more specific.
+ *
+ *
+ * @see Quarkus Issue 7883
+ *
+ */
+@Slf4j
+public abstract class AbstractExceptionMapper {
+
+ protected boolean exposeInternalErrorDetails = false;
+
+ @Context
+ UriInfo uriInfo;
+
+ protected void logError(Exception exception) {
+
+ log.error("Exception:{},URL:{},ERROR:{}", exception.getClass().getCanonicalName(), this.uriInfo.getRequestUri(),
+ exception.getMessage());
+ }
+
+ protected Response createResponse(int status, String errorCode, Exception exception) {
+
+ Map jsonMap = new HashMap<>();
+ jsonMap.put("code", errorCode);
+ if (this.exposeInternalErrorDetails) {
+ jsonMap.put("message", getExposedErrorDetails(exception));
+ } else {
+ jsonMap.put("message", exception.getMessage());
+ }
+ jsonMap.put("uri", this.uriInfo.getPath());
+ jsonMap.put("uuid", UUID.randomUUID());
+ jsonMap.put("timestamp", ZonedDateTime.now().toString());
+ return Response.status(status).type(MediaType.APPLICATION_JSON).entity(jsonMap).build();
+ }
+
+ 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.getLocalizedMessage());
+ e = e.getCause();
+ }
+ return buffer.toString();
+ }
+
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/NotFoundExceptionMapper.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/NotFoundExceptionMapper.java
new file mode 100644
index 00000000..29c33d81
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/NotFoundExceptionMapper.java
@@ -0,0 +1,19 @@
+package com.devonfw.quarkus.general.rest.exception.mapper;
+
+import javax.ws.rs.NotFoundException;
+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;
+
+@Provider
+public class NotFoundExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(NotFoundException exception) {
+
+ logError(exception);
+
+ return createResponse(Status.NOT_FOUND.getStatusCode(), exception.getClass().getSimpleName(), exception);
+ }
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/RuntimeExceptionMapper.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/RuntimeExceptionMapper.java
new file mode 100644
index 00000000..051626cf
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/RuntimeExceptionMapper.java
@@ -0,0 +1,46 @@
+package com.devonfw.quarkus.general.rest.exception.mapper;
+
+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 com.devonfw.quarkus.general.rest.exception.ApplicationBusinessException;
+
+@Provider
+public class RuntimeExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(RuntimeException exception) {
+
+ logError(exception);
+
+ if (exception instanceof ApplicationBusinessException) {
+ return createResponse((ApplicationBusinessException) exception);
+ } else if (exception instanceof WebApplicationException) {
+ return createResponse((WebApplicationException) exception);
+ }
+
+ return createResponse(exception);
+ }
+
+ private Response createResponse(ApplicationBusinessException exception) {
+
+ int status = exception.getStatusCode() == null ? Status.BAD_REQUEST.getStatusCode() : exception.getStatusCode();
+ return createResponse(status, exception.getCode(), exception);
+ }
+
+ private Response createResponse(WebApplicationException exception) {
+
+ Status status = Status.fromStatusCode(exception.getResponse().getStatus());
+ return createResponse(status.getStatusCode(), exception.getClass().getSimpleName(), exception);
+ }
+
+ private Response createResponse(Exception exception) {
+
+ return createResponse(Status.INTERNAL_SERVER_ERROR.getStatusCode(), exception.getClass().getSimpleName(),
+ exception);
+ }
+
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/UnauthorizedExceptionMapper.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/UnauthorizedExceptionMapper.java
new file mode 100644
index 00000000..8097eed4
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/UnauthorizedExceptionMapper.java
@@ -0,0 +1,21 @@
+package com.devonfw.quarkus.general.rest.exception.mapper;
+
+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 io.quarkus.security.UnauthorizedException;
+
+@Provider
+public class UnauthorizedExceptionMapper extends AbstractExceptionMapper
+ implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(UnauthorizedException exception) {
+
+ logError(exception);
+
+ return createResponse(Status.UNAUTHORIZED.getStatusCode(), exception.getClass().getSimpleName(), exception);
+ }
+}
diff --git a/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/ValidationExceptionMapper.java b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/ValidationExceptionMapper.java
new file mode 100644
index 00000000..6d20a7ee
--- /dev/null
+++ b/src/main/java/com/devonfw/quarkus/general/rest/exception/mapper/ValidationExceptionMapper.java
@@ -0,0 +1,19 @@
+package com.devonfw.quarkus.general.rest.exception.mapper;
+
+import javax.validation.ValidationException;
+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;
+
+@Provider
+public class ValidationExceptionMapper extends AbstractExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(ValidationException exception) {
+
+ logError(exception);
+
+ return createResponse(Status.BAD_REQUEST.getStatusCode(), exception.getClass().getSimpleName(), exception);
+ }
+}
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductFragmentImpl.java b/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductFragmentImpl.java
index f657c346..8a253126 100644
--- a/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductFragmentImpl.java
+++ b/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductFragmentImpl.java
@@ -1,6 +1,5 @@
package com.devonfw.quarkus.productmanagement.domain.repo;
-import static com.devonfw.quarkus.productmanagement.utils.StringUtils.isEmpty;
import static java.util.Objects.isNull;
import java.util.ArrayList;
@@ -9,6 +8,7 @@
import javax.inject.Inject;
import javax.persistence.EntityManager;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -30,7 +30,7 @@ public Page findByCriteria(ProductSearchCriteriaDto searchCriteri
QProductEntity product = QProductEntity.productEntity;
List predicates = new ArrayList<>();
- if (!isEmpty(searchCriteria.getTitle())) {
+ if (!StringUtils.isEmpty(searchCriteria.getTitle())) {
predicates.add(product.title.eq(searchCriteria.getTitle()));
}
if (!isNull(searchCriteria.getPrice())) {
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductRepository.java b/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductRepository.java
index 39a68d4f..a5664aa1 100644
--- a/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductRepository.java
+++ b/src/main/java/com/devonfw/quarkus/productmanagement/domain/repo/ProductRepository.java
@@ -1,13 +1,13 @@
package com.devonfw.quarkus.productmanagement.domain.repo;
import org.springframework.data.domain.Page;
+import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import com.devonfw.quarkus.productmanagement.domain.model.ProductEntity;
-public interface ProductRepository extends CrudRepository, ProductFragment {
+public interface ProductRepository extends JpaRepository, ProductFragment {
@Query("select a from ProductEntity a where title = :title")
ProductEntity findByTitle(@Param("title") String title);
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/ProductRestService.java b/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/ProductRestService.java
index 42c2220a..5e85985b 100644
--- a/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/ProductRestService.java
+++ b/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/ProductRestService.java
@@ -1,30 +1,30 @@
package com.devonfw.quarkus.productmanagement.rest.v1;
-import static com.devonfw.quarkus.productmanagement.utils.StringUtils.isEmpty;
import static javax.ws.rs.core.Response.created;
-import static javax.ws.rs.core.Response.status;
import java.util.Optional;
import javax.inject.Inject;
+import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
-import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
+import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.springframework.data.domain.Page;
+import com.devonfw.quarkus.general.rest.exception.InvalidParameterException;
import com.devonfw.quarkus.productmanagement.domain.model.ProductEntity;
import com.devonfw.quarkus.productmanagement.domain.repo.ProductRepository;
import com.devonfw.quarkus.productmanagement.rest.v1.mapper.ProductMapper;
@@ -56,11 +56,7 @@ public Page getAllOrderedByTitle() {
}
@POST
- public Response createNewProduct(ProductDto product) {
-
- if (isEmpty(product.getTitle())) {
- throw new WebApplicationException("Title was not set on request.", 400);
- }
+ public Response createNewProduct(@Valid ProductDto product) {
ProductEntity productEntity = this.productRepository.save(this.productMapper.map(product));
@@ -83,11 +79,15 @@ public Page findProducts(ProductSearchCriteriaDto searchCriteria) {
@Path("{id}")
public ProductDto getProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {
+ if (!StringUtils.isNumeric(id)) {
+ throw new InvalidParameterException("Unable to parse ID: " + id);
+ }
+
Optional product = this.productRepository.findById(Long.valueOf(id));
- if (product.isPresent()) {
- return this.productMapper.map(product.get());
+ if (!product.isPresent()) {
+ throw new NotFoundException();
}
- return null;
+ return this.productMapper.map(product.get());
}
@GET
@@ -99,10 +99,16 @@ public ProductDto getProductByTitle(@PathParam("title") String title) {
@DELETE
@Path("{id}")
- public Response deleteProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {
+ public void deleteProductById(@Parameter(description = "Product unique id") @PathParam("id") String id) {
- this.productRepository.deleteById(Long.valueOf(id));
- return status(Status.NO_CONTENT.getStatusCode()).build();
- }
+ if (!StringUtils.isNumeric(id)) {
+ throw new InvalidParameterException("Unable to parse ID: " + id);
+ }
+ try {
+ this.productRepository.deleteById(Long.valueOf(id));
+ } catch (IllegalArgumentException e) {
+ throw new NotFoundException();
+ }
+ }
}
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/model/ProductDto.java b/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/model/ProductDto.java
index 1f11e7c7..064be3f6 100644
--- a/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/model/ProductDto.java
+++ b/src/main/java/com/devonfw/quarkus/productmanagement/rest/v1/model/ProductDto.java
@@ -2,28 +2,21 @@
import java.math.BigDecimal;
-import org.eclipse.microprofile.openapi.annotations.media.Schema;
+import javax.validation.constraints.Size;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
import lombok.Getter;
-import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
public class ProductDto extends AbstractDto {
- @Schema(nullable = false, description = "Product title", minLength = 3, maxLength = 500)
+ @Size(min = 3, max = 500)
private String title;
- @Schema(description = "Product description", minLength = 3, maxLength = 500)
+ @Size(min = 3, max = 4000)
private String description;
- @Schema(description = "Product price")
private BigDecimal price;
}
diff --git a/src/main/java/com/devonfw/quarkus/productmanagement/utils/StringUtils.java b/src/main/java/com/devonfw/quarkus/productmanagement/utils/StringUtils.java
deleted file mode 100644
index effc93ca..00000000
--- a/src/main/java/com/devonfw/quarkus/productmanagement/utils/StringUtils.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.devonfw.quarkus.productmanagement.utils;
-
-public class StringUtils {
- public static boolean isEmpty(String str) {
-
- return (str == null || "".equals(str));
- }
-}
diff --git a/src/test/java/com/devonfw/demoquarkus/rest/v1/ProductRestServiceTest.java b/src/test/java/com/devonfw/demoquarkus/rest/v1/ProductRestServiceTest.java
index dc3a7325..8fc99ba5 100644
--- a/src/test/java/com/devonfw/demoquarkus/rest/v1/ProductRestServiceTest.java
+++ b/src/test/java/com/devonfw/demoquarkus/rest/v1/ProductRestServiceTest.java
@@ -37,13 +37,6 @@ void getAll() {
@Test
@Order(2)
- void getNonExistingTest() {
-
- given().when().contentType(MediaType.APPLICATION_JSON).get("/product/v1/99999").then().log().all().statusCode(204);
- }
-
- @Test
- @Order(3)
void createNewProduct() {
ProductDto product = new ProductDto();
@@ -59,7 +52,7 @@ void createNewProduct() {
}
@Test
- @Order(4)
+ @Order(3)
public void testGetById() {
given().when().log().all().contentType(MediaType.APPLICATION_JSON).get("/product/v1/1").then().statusCode(200)
@@ -68,11 +61,45 @@ public void testGetById() {
}
@Test
- @Order(5)
+ @Order(4)
public void deleteById() {
given().when().log().all().contentType(MediaType.APPLICATION_JSON).delete("/product/v1/1").then().statusCode(204);
- given().when().log().all().contentType(MediaType.APPLICATION_JSON).get("/product/v1/1").then().statusCode(204);
+
+ // after deletion it should be deleted
+ given().when().log().all().contentType(MediaType.APPLICATION_JSON).get("/product/v1/1").then().statusCode(404);
+
+ // delete again should fail
+ given().when().log().all().contentType(MediaType.APPLICATION_JSON).delete("/product/v1/1").then().statusCode(404);
+ }
+
+ @Test
+ @Order(5)
+ void businessExceptionTest() {
+
+ given().when().contentType(MediaType.APPLICATION_JSON).get("/product/v1/doesnotexist").then().log().all()
+ .statusCode(422).extract().response();
+ }
+
+ @Test
+ @Order(6)
+ void notFoundExceptionTest() {
+
+ given().when().contentType(MediaType.APPLICATION_JSON).get("/product/v1/0").then().log().all().statusCode(404)
+ .extract().response();
+ }
+
+ @Test
+ @Order(7)
+ void validationExceptionTest() {
+
+ // Create a product that does not match the validation rules
+ ProductDto product = new ProductDto();
+ product.setTitle("");
+
+ given().when().body(product).contentType(MediaType.APPLICATION_JSON).post("/product/v1").then().log().all()
+ .statusCode(400);
+
}
}
\ No newline at end of file