From ae0e1e639087910ce92d277b9add807c53e35594 Mon Sep 17 00:00:00 2001 From: Andrej Petras Date: Fri, 19 Jan 2024 15:34:41 +0100 Subject: [PATCH] feat: add utility method for search string like or equal predicate (#122) * feat: add DAO optimistic lock exception * feat: add utility for search query like or equal predicate --- README.md | 14 +- extensions/jpa/runtime/pom.xml | 12 ++ .../tkit/quarkus/jpa/daos/AbstractDAO.java | 13 +- .../jpa/exceptions/ConstraintException.java | 20 +++ .../quarkus/jpa/utils/QueryCriteriaUtil.java | 153 +++++++++++++++++- .../jpa/utils/QueryCriteriaUtilTest.java | 81 ++++++++++ .../tkit/quarkus/jpa/test/UserDAOTest.java | 4 +- extensions/rest-dto/README.md | 2 + .../quarkus/rs/exceptions/RestException.java | 1 + .../rs/mappers/DefaultExceptionMapper.java | 1 + .../rs/models/AbstractTraceableDTO.java | 1 + .../rs/models/BusinessTraceableDTO.java | 1 + .../tkit/quarkus/rs/models/PageResultDTO.java | 1 + .../quarkus/rs/models/RestExceptionDTO.java | 1 + .../tkit/quarkus/rs/models/TraceableDTO.java | 1 + .../quarkus/rs/resources/ResourceManager.java | 1 + .../java/org/tkit/quarkus/it/jpa/UserDTO.java | 2 + .../quarkus/it/jpa/UserRestController.java | 27 ++++ .../it/jpa/UserRestControllerTest.java | 62 ++++++- it/jpa/src/test/resources/data/test.xml | 4 +- it/jpa/src/test/resources/data/update.xml | 5 + pom.xml | 4 +- 22 files changed, 380 insertions(+), 31 deletions(-) create mode 100644 extensions/jpa/runtime/src/test/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtilTest.java create mode 100644 it/jpa/src/test/resources/data/update.xml diff --git a/README.md b/README.md index 675bbe0..1a7856f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Include the component in your project by including the corresponding dependency. | Log RS | tkit-quarkus-log-rs | | Quarkus extension for HTTP request logging (client & server). | [Link](extensions/log/rs) | | Log JSON | tkit-quarkus-log-json | | Custom JSON log formatter that provides additional features not included in [Official Quarkus json logger](https://quarkus.io/guides/logging#json-logging). Make sure you only include this if you need those extra features, otherwise use the official extension. | [Link](extensions/log/json) | | Rest | tkit-quarkus-rest | | Helper classes for JAX-RS and Jackson. | [Link](extensions/rest-dto) | -| Rest DTO | tkit-quarkus-rest-dto | | Helper classes for REST - model mapping, exception handling, DTOs. | [Link](extensions/rest) | +| Rest DTO | tkit-quarkus-rest-dto | > DEPRECATED | Helper classes for REST - model mapping, exception handling, DTOs. | [Link](extensions/rest) | | Test data import | tkit-quarkus-test-db-import | | Test extension for data import from excel into database during unit tests. | [Link](extensions/test-db-import) | | Security | tkit-quarkus-security | | Enhanced security configuration | [Link](extensions/security) | @@ -65,10 +65,7 @@ Include the component in your project by including the corresponding dependency. If you have used previous versions of tkit quarkus libraries (mvn groupId `org.tkit.quarkus`) then there are a few breaking changes in this new version, however the migration is straightforward: -1. Quarkus 2.x -Tkit Quarkus libs only support Quarkus version 2. Check the [official guide](https://github.com/quarkusio/quarkus/wiki/Migration-Guide-2.0) for migration instructions. - -2. Change maven imports +#### Change maven imports Group id of the libraries has changed to `org.tkit.quarkus.lib`. Also, prefer the use of bom import, to ensure version compatibility. So if your current pom.xml looks sth like this: ```xml @@ -97,7 +94,7 @@ Change it to: ``` -3. Update configuration +#### Update configuration All extensions and libraries now have unified configuration properties structure, starting with `tkit.` prefix, some keys have been renamed or otherwise updated. Check the table bellow for config property migration: | Old | New | Note | @@ -105,7 +102,7 @@ All extensions and libraries now have unified configuration properties structure | `quarkus.tkit.log.ignore.pattern` | `tkit.log.cdi.auto-discovery.ignore.pattern` | | | `quarkus.tkit.log.packages` | `tkit.log.cdi.auto-discovery.packages` | In order to enable auto binding of logging extension, you must add property `tkit.log.cdi.auto-discovery.enabled=true` | -4. Default behavior changes +#### Default behavior changes Logging: CDI logging now only logs end of business methods (success or error) to reduce logging verbosity. If you restore the behavior and still log start method invocations, set the property: `tkit.log.cdi.start.enabled=true` @@ -122,7 +119,8 @@ New behavior: [com.acme.dom.dao.SomeBean] someMethod(param):SomeResultClass [0.035s] ``` -5. Use `modificationCount` instead of `version` when working with `TraceableEntity`. Therefore, annotations like `@Mapping(target = "version", ignore = true)` should be changed to `@Mapping(target = "modificationCount", ignore = true)`. +#### JPA ModificationCount +Use `modificationCount` instead of `version` when working with `TraceableEntity`. Therefore, annotations like `@Mapping(target = "version", ignore = true)` should be changed to `@Mapping(target = "modificationCount", ignore = true)`. ## Contributors ✨ diff --git a/extensions/jpa/runtime/pom.xml b/extensions/jpa/runtime/pom.xml index 0fde9d3..280e3a8 100644 --- a/extensions/jpa/runtime/pom.xml +++ b/extensions/jpa/runtime/pom.xml @@ -31,6 +31,18 @@ tkit-quarkus-context ${project.version} + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit5-mockito + test + diff --git a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/daos/AbstractDAO.java b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/daos/AbstractDAO.java index d102fc7..206469f 100644 --- a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/daos/AbstractDAO.java +++ b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/daos/AbstractDAO.java @@ -23,11 +23,7 @@ import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.EntityManager; -import jakarta.persistence.LockModeType; -import jakarta.persistence.Query; -import jakarta.persistence.TypedQuery; +import jakarta.persistence.*; import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; @@ -539,17 +535,20 @@ protected void lock(T entity, LockModeType lockMode) { * @return the corresponding service exception. */ @SuppressWarnings("squid:S1872") - protected DAOException handleConstraint(Exception ex, Enum key) { + protected RuntimeException handleConstraint(Exception ex, Enum key) { if (ex instanceof ConstraintException ce) { return ce; } + if (ex instanceof OptimisticLockException ole) { + return ole; + } if (ex instanceof ConstraintViolationException cve) { var msg = cve.getErrorMessage(); if (msg != null) { msg = msg.replaceAll("\n", "").replaceAll("\"", "'"); } var re = new ConstraintException(msg, key, ex, entityName); - re.addParameter("constraintName", cve.getConstraintName()); + re.addConstraintName(cve.getConstraintName()); return re; } return new DAOException(key, ex, entityName); diff --git a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/exceptions/ConstraintException.java b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/exceptions/ConstraintException.java index bb6e194..d32897a 100644 --- a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/exceptions/ConstraintException.java +++ b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/exceptions/ConstraintException.java @@ -27,6 +27,8 @@ public class ConstraintException extends DAOException { */ private static final String PARAMETER = "constraint"; + private static final String NAME = "constraintName"; + /** * The default constructor. * @@ -40,6 +42,24 @@ public ConstraintException(String constraints, Enum messageKey, Throwable cau addParameter(PARAMETER, constraints); } + /** + * Sets the constraint name. + * + * @param name the name of the constraint. + */ + public void addConstraintName(String name) { + addParameter(NAME, name); + } + + /** + * Gets the constraints name. + * + * @return the constraints name. + */ + public String getConstraintName() { + return (String) namedParameters.get(NAME); + } + /** * Gets the constraints message. * diff --git a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtil.java b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtil.java index fb5f074..efa516c 100644 --- a/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtil.java +++ b/extensions/jpa/runtime/src/main/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtil.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Expression; @@ -30,6 +31,13 @@ */ public class QueryCriteriaUtil { + /** + * Custom query like characters. + */ + private static final Map DEFAULT_LIKE_MAPPING_CHARACTERS = Map.of( + "*", "%", + "?", "_"); + /** * The default constructor. */ @@ -38,7 +46,7 @@ private QueryCriteriaUtil() { } /** - * Wildcard the search string with case insensitive {@code true}. + * Wildcard the search string with case-insensitive {@code true}. * * @param searchString the search string. * @return the corresponding search string. @@ -89,7 +97,7 @@ public static Predicate inClause(Expression path, Collection values, Crite subList.clear(); } predicates.add(path.in(valuesList)); - result = cb.or(predicates.toArray(new Predicate[predicates.size()])); + result = cb.or(predicates.toArray(new Predicate[0])); } return result; } @@ -114,7 +122,7 @@ public static Predicate notInClause(Expression path, Collection values, Cr subList.clear(); } predicates.add(cb.not(path.in(valuesList))); - result = cb.and(predicates.toArray(new Predicate[predicates.size()])); + result = cb.and(predicates.toArray(new Predicate[0])); } return result; } @@ -129,6 +137,7 @@ public static Predicate notInClause(Expression path, Collection values, Cr * @param parameters the parameters to be added from the IN clause * @return the query string with the IN clause */ + @Deprecated public static String inClause(String attribute, String attributeName, Collection values, Map parameters) { StringBuilder sb = new StringBuilder(); @@ -159,6 +168,7 @@ public static String inClause(String attribute, String attributeName, Collection * @param parameters the parameters to be added from the NOT IN clause * @return the query string with the NOT IN clause */ + @Deprecated public static String notInClause(String attribute, String attributeName, Collection values, Map parameters) { StringBuilder sb = new StringBuilder(); @@ -178,4 +188,141 @@ public static String notInClause(String attribute, String attributeName, Collect parameters.put(attributeName, valuesList); return sb.toString(); } + + /** + * Add a search predicate to the list of predicates. + * + * @param predicates - list of predicates + * @param criteriaBuilder - CriteriaBuilder + * @param column - column Path [root.get(Entity_.attribute)] + * @param searchString - string to search. if Contains [*,?] like will be used + * @param caseInsensitive - true in case of insensitive search (db column and search string are given to lower case) + * @return {@code true} add predicate to the list + */ + public static boolean addSearchStringPredicate(List predicates, CriteriaBuilder criteriaBuilder, + Expression column, + String searchString, final boolean caseInsensitive) { + if (predicates == null) { + return false; + } + var predicate = createSearchStringPredicate(criteriaBuilder, column, searchString, caseInsensitive); + if (predicate == null) { + return false; + } + + return predicates.add(predicate); + } + + /** + * Add a search predicate as a case of insensitive search to the list of predicates. + * + * @param predicates - list of predicates + * @param criteriaBuilder - CriteriaBuilder + * @param column - column Path [root.get(Entity_.attribute)] + * @param searchString - string to search. if Contains [*,?] like will be used + * @return {@code true} add predicate to the list + */ + public static boolean addSearchStringPredicate(List predicates, CriteriaBuilder criteriaBuilder, + Expression column, + String searchString) { + if (predicates == null) { + return false; + } + var predicate = createSearchStringPredicate(criteriaBuilder, column, searchString, true); + if (predicate == null) { + return false; + } + + return predicates.add(predicate); + } + + /** + * Create a search predicate as a case of insensitive search. + * + * @param criteriaBuilder - CriteriaBuilder + * @param column - column Path [root.get(Entity_.attribute)] + * @param searchString - string to search. if Contains [*,?] like will be used + * @return LIKE or EQUAL Predicate according to the search string + */ + public static Predicate createSearchStringPredicate(CriteriaBuilder criteriaBuilder, Expression column, + String searchString) { + return createSearchStringPredicate(criteriaBuilder, column, searchString, true); + } + + /** + * Create a search predicate. + * + * @param criteriaBuilder - CriteriaBuilder + * @param column - column Path [root.get(Entity_.attribute)] + * @param searchString - string to search. if Contains [*,?] like will be used + * @param caseInsensitive - true in case of insensitive search (db column and search string are given to lower case) + * @return LIKE or EQUAL Predicate according to the search string + */ + public static Predicate createSearchStringPredicate(CriteriaBuilder criteriaBuilder, Expression column, + String searchString, final boolean caseInsensitive) { + return createSearchStringPredicate(criteriaBuilder, column, searchString, caseInsensitive, + QueryCriteriaUtil::defaultReplaceFunction, DEFAULT_LIKE_MAPPING_CHARACTERS); + } + + /** + * Create a search predicate. + * + * @param criteriaBuilder - CriteriaBuilder + * @param column - column Path [root.get(Entity_.attribute)] + * @param searchString - string to search. if Contains [*,?] like will be used + * @param caseInsensitive - true in case of insensitive search (db column and search string are given to lower case) + * @param replaceFunction - replace special character function for characters in the searchString + * @param likeMapping - map of like query mapping characters ['*','%', ...] + * @return LIKE or EQUAL Predicate according to the search string + */ + public static Predicate createSearchStringPredicate(CriteriaBuilder criteriaBuilder, Expression column, + String searchString, final boolean caseInsensitive, + Function replaceFunction, + Map likeMapping) { + + if (searchString == null || searchString.isBlank()) { + return null; + } + + // replace function for special characters + if (replaceFunction != null) { + searchString = replaceFunction.apply(searchString); + } + + // case insensitive + Expression columnDefinition = column; + if (caseInsensitive) { + searchString = searchString.toLowerCase(); + columnDefinition = criteriaBuilder.lower(column); + } + + // check for like characters + boolean like = false; + if (likeMapping != null) { + for (Map.Entry item : likeMapping.entrySet()) { + if (searchString.contains(item.getKey())) { + searchString = searchString.replace(item.getKey(), item.getValue()); + like = true; + } + } + } + + // like predicate + if (like) { + return criteriaBuilder.like(columnDefinition, searchString); + } + + // equal predicate + return criteriaBuilder.equal(columnDefinition, searchString); + } + + /** + * Escape the extra DB characters + */ + public static String defaultReplaceFunction(String searchString) { + return searchString + .replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_"); + } } diff --git a/extensions/jpa/runtime/src/test/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtilTest.java b/extensions/jpa/runtime/src/test/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtilTest.java new file mode 100644 index 0000000..3cb25aa --- /dev/null +++ b/extensions/jpa/runtime/src/test/java/org/tkit/quarkus/jpa/utils/QueryCriteriaUtilTest.java @@ -0,0 +1,81 @@ +package org.tkit.quarkus.jpa.utils; + +import static org.mockito.ArgumentMatchers.any; + +import jakarta.persistence.criteria.*; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +class QueryCriteriaUtilTest { + + @Test + void createSearchStringPredicateTest() { + + final var result = new TestResult(); + + Predicate predicate = Mockito.mock(Predicate.class); + CriteriaBuilder cb = Mockito.mock(CriteriaBuilder.class); + Mockito.when(cb.lower(any())).thenReturn(null); + Mockito.when(cb.like(any(), (String) any())).thenAnswer(invocation -> { + result.like = true; + result.searchString = invocation.getArgument(1); + return predicate; + }); + Mockito.when(cb.equal(any(), (String) any())).thenAnswer((Answer) invocation -> { + result.equal = true; + result.searchString = invocation.getArgument(1); + return predicate; + }); + + Expression expression = Mockito.mock(Expression.class); + Mockito.when(cb.lower(any())).thenAnswer(invocation -> { + result.lower = false; + return expression; + }); + + Predicate p = QueryCriteriaUtil.createSearchStringPredicate(cb, expression, "searchText", false); + Assertions.assertNotNull(p); + Assertions.assertFalse(result.like); + Assertions.assertTrue(result.equal); + Assertions.assertEquals("searchText", result.searchString); + + result.reset(); + p = QueryCriteriaUtil.createSearchStringPredicate(cb, expression, "ThisIsNotSearchText", true); + Assertions.assertNotNull(p); + Assertions.assertFalse(result.like); + Assertions.assertTrue(result.equal); + Assertions.assertEquals("thisisnotsearchtext", result.searchString); + + result.reset(); + p = QueryCriteriaUtil.createSearchStringPredicate(cb, expression, "?ThisIsNotSearchText", true); + Assertions.assertNotNull(p); + Assertions.assertFalse(result.equal); + Assertions.assertTrue(result.like); + Assertions.assertEquals("_thisisnotsearchtext", result.searchString); + + result.reset(); + p = QueryCriteriaUtil.createSearchStringPredicate(cb, expression, "?ThisIsNotSearchText*", true); + Assertions.assertNotNull(p); + Assertions.assertFalse(result.equal); + Assertions.assertTrue(result.like); + Assertions.assertEquals("_thisisnotsearchtext%", result.searchString); + } + + public static class TestResult { + boolean like = false; + boolean equal = false; + boolean lower = false; + + String searchString = null; + + void reset() { + like = false; + equal = false; + lower = false; + searchString = null; + } + } +} diff --git a/extensions/jpa/tests/src/test/java/org/tkit/quarkus/jpa/test/UserDAOTest.java b/extensions/jpa/tests/src/test/java/org/tkit/quarkus/jpa/test/UserDAOTest.java index 08ee5b5..ded3abc 100644 --- a/extensions/jpa/tests/src/test/java/org/tkit/quarkus/jpa/test/UserDAOTest.java +++ b/extensions/jpa/tests/src/test/java/org/tkit/quarkus/jpa/test/UserDAOTest.java @@ -7,6 +7,7 @@ import java.util.stream.Stream; import jakarta.inject.Inject; +import jakarta.persistence.OptimisticLockException; import jakarta.persistence.criteria.Order; import jakarta.transaction.Transactional; @@ -19,7 +20,6 @@ import org.tkit.quarkus.jpa.daos.Page; import org.tkit.quarkus.jpa.daos.PageResult; import org.tkit.quarkus.jpa.daos.PagedQuery; -import org.tkit.quarkus.jpa.exceptions.DAOException; import org.tkit.quarkus.jpa.models.TraceableEntity; import io.quarkus.test.junit.QuarkusTest; @@ -48,7 +48,7 @@ public void updateUserTest() { userDAO.update(loaded); user.setName("update-name"); - Assertions.assertThrows(DAOException.class, () -> { + Assertions.assertThrows(OptimisticLockException.class, () -> { userDAO.update(user); }); } diff --git a/extensions/rest-dto/README.md b/extensions/rest-dto/README.md index 2403ed0..eee3f77 100644 --- a/extensions/rest-dto/README.md +++ b/extensions/rest-dto/README.md @@ -1,5 +1,7 @@ # tkit-quarkus-rest-dto +> tkit-quarkus-rest-dto is deprecated. It will be removed in the next major release. + Helper classes for JAX-RS - model mapping, exception handling, DTOs. Maven dependency diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/exceptions/RestException.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/exceptions/RestException.java index 765fa87..24e8185 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/exceptions/RestException.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/exceptions/RestException.java @@ -12,6 +12,7 @@ * The REST exception. The DTO for this class {@link org.tkit.quarkus.rs.models.RestExceptionDTO} * THe exception mapper {@link org.tkit.quarkus.rs.mappers.DefaultExceptionMapper} */ +@Deprecated public class RestException extends RuntimeException { /** diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/mappers/DefaultExceptionMapper.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/mappers/DefaultExceptionMapper.java index 8241bf7..e53d232 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/mappers/DefaultExceptionMapper.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/mappers/DefaultExceptionMapper.java @@ -24,6 +24,7 @@ /** * The default exception mapper with priority {@code PRIORITY}. */ +@Deprecated @Provider @Priority(DefaultExceptionMapper.PRIORITY) public class DefaultExceptionMapper implements ExceptionMapper { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/AbstractTraceableDTO.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/AbstractTraceableDTO.java index 72e9149..219b33b 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/AbstractTraceableDTO.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/AbstractTraceableDTO.java @@ -23,6 +23,7 @@ /** * The persistent entity interface. */ +@Deprecated @RegisterForReflection public abstract class AbstractTraceableDTO implements Serializable { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/BusinessTraceableDTO.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/BusinessTraceableDTO.java index eeb4913..32ee7c4 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/BusinessTraceableDTO.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/BusinessTraceableDTO.java @@ -19,6 +19,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; +@Deprecated @RegisterForReflection public class BusinessTraceableDTO extends AbstractTraceableDTO { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/PageResultDTO.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/PageResultDTO.java index 402e5e1..7ac923c 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/PageResultDTO.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/PageResultDTO.java @@ -19,6 +19,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection; +@Deprecated @RegisterForReflection public class PageResultDTO { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/RestExceptionDTO.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/RestExceptionDTO.java index 3562ada..ace2728 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/RestExceptionDTO.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/RestExceptionDTO.java @@ -24,6 +24,7 @@ /** * The rest exception DTO model. */ +@Deprecated @RegisterForReflection public class RestExceptionDTO { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/TraceableDTO.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/TraceableDTO.java index 05d4d05..6fea9f9 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/TraceableDTO.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/models/TraceableDTO.java @@ -23,6 +23,7 @@ /** * The persistent entity with string GUID. */ +@Deprecated @RegisterForReflection public class TraceableDTO extends AbstractTraceableDTO { diff --git a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/resources/ResourceManager.java b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/resources/ResourceManager.java index 25b8583..c8b042c 100644 --- a/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/resources/ResourceManager.java +++ b/extensions/rest-dto/src/main/java/org/tkit/quarkus/rs/resources/ResourceManager.java @@ -14,6 +14,7 @@ /** * The resource manager for messages. */ +@Deprecated public class ResourceManager { private static final Logger log = LoggerFactory.getLogger(ResourceManager.class); diff --git a/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserDTO.java b/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserDTO.java index 7c90906..f4e8ab7 100644 --- a/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserDTO.java +++ b/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserDTO.java @@ -2,6 +2,8 @@ public class UserDTO { + public Integer modificationCount; + public String id; public String username; diff --git a/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserRestController.java b/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserRestController.java index e0637c4..a981e3c 100644 --- a/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserRestController.java +++ b/it/jpa/src/main/java/org/tkit/quarkus/it/jpa/UserRestController.java @@ -1,6 +1,7 @@ package org.tkit.quarkus.it.jpa; import jakarta.inject.Inject; +import jakarta.persistence.OptimisticLockException; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -24,6 +25,7 @@ public Response get(@PathParam("id") String id) { return Response.status(Response.Status.NOT_FOUND).build(); } UserDTO dto = new UserDTO(); + dto.modificationCount = user.getModificationCount(); dto.id = user.getId(); dto.username = user.username; dto.email = user.email; @@ -41,6 +43,31 @@ public Response create(UserDTO dto) { return Response.ok(user).build(); } + @PUT + @Path("{id}") + @LogRestService + public Response update(@PathParam("id") String id, UserDTO dto) { + var user = dao.findById(id); + if (user == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + user.setModificationCount(dto.modificationCount); + user.username = dto.username; + user.email = dto.email; + try { + user = dao.update(user); + } catch (OptimisticLockException e) { + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + UserDTO r = new UserDTO(); + r.modificationCount = user.getModificationCount(); + r.id = user.getId(); + r.username = user.username; + r.email = user.email; + + return Response.ok(r).build(); + } + @GET @Path("ping2") public Response ping2() { diff --git a/it/jpa/src/test/java/org/tkit/quarkus/it/jpa/UserRestControllerTest.java b/it/jpa/src/test/java/org/tkit/quarkus/it/jpa/UserRestControllerTest.java index bfc4522..e67e6d3 100644 --- a/it/jpa/src/test/java/org/tkit/quarkus/it/jpa/UserRestControllerTest.java +++ b/it/jpa/src/test/java/org/tkit/quarkus/it/jpa/UserRestControllerTest.java @@ -8,16 +8,18 @@ import org.junit.jupiter.api.Test; import org.tkit.quarkus.test.WithDBData; +import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; @QuarkusTest +@TestHTTPEndpoint(UserRestController.class) public class UserRestControllerTest extends AbstractTest { @Test public void ping() { given() - .get("users/ping") + .get("ping") .then() .statusCode(Response.Status.OK.getStatusCode()); } @@ -25,7 +27,7 @@ public void ping() { @Test public void test() { given() - .get("users/test") + .get("test") .then() .statusCode(Response.Status.OK.getStatusCode()); } @@ -43,7 +45,7 @@ public void notfound() { public void importDataTest() throws InterruptedException { given() .pathParam("id", "GUID_3") - .get("users/{id}") + .get("{id}") .then() .statusCode(Response.Status.OK.getStatusCode()); } @@ -53,16 +55,62 @@ public void importDataTest() throws InterruptedException { public void importRemoteXmlDataTest() throws InterruptedException { given() .pathParam("id", "GUID_1") - .get("users/{id}") + .get("{id}") .then() .statusCode(Response.Status.OK.getStatusCode()); } + @Test + @WithDBData(value = { "data/update.xml" }) + public void updateUserTest() { + var dto = given() + .get("U_GUID_3") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserDTO.class); + + var update = new UserDTO(); + update.username = dto.username; + update.email = dto.email + "+update"; + update.modificationCount = dto.modificationCount; + + given() + .body(update) + .contentType(ContentType.JSON) + .put(dto.id) + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserDTO.class); + + dto = given() + .get(dto.id) + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract().as(UserDTO.class); + + update.username = dto.username; + update.email = dto.email + "+update"; + update.modificationCount = 1; + + var msg = given() + .body(update) + .contentType(ContentType.JSON) + .put(dto.id) + .then() + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) + .extract().asString(); + + Assertions.assertNotNull(msg); + Assertions.assertEquals( + "Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.tkit.quarkus.it.jpa.User#U_GUID_3]", + msg); + } + @Test public void createUserTest() { given() .pathParam("id", "1234") - .get("users/{id}") + .get("{id}") .then() .statusCode(Response.Status.NOT_FOUND.getStatusCode()); @@ -72,7 +120,7 @@ public void createUserTest() { User tmp = given() .contentType(ContentType.JSON) .body(user) - .post("users") + .post() .then() .statusCode(Response.Status.OK.getStatusCode()) .extract().body().as(User.class); @@ -80,7 +128,7 @@ public void createUserTest() { User find = given() .contentType(ContentType.JSON) .pathParam("id", tmp.getId()) - .get("users/{id}") + .get("{id}") .then() .statusCode(Response.Status.OK.getStatusCode()) .extract().body().as(User.class); diff --git a/it/jpa/src/test/resources/data/test.xml b/it/jpa/src/test/resources/data/test.xml index b03b782..4cd1219 100644 --- a/it/jpa/src/test/resources/data/test.xml +++ b/it/jpa/src/test/resources/data/test.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/it/jpa/src/test/resources/data/update.xml b/it/jpa/src/test/resources/data/update.xml new file mode 100644 index 0000000..8d2e39e --- /dev/null +++ b/it/jpa/src/test/resources/data/update.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 82b8bf8..e04035c 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 3.6.4 - 6.4.1.Final + 6.4.2.Final 1.2.0 3.1.6 @@ -67,7 +67,7 @@ 17 true 3.12.1 - 3.2.3 + 3.2.5 2.23.0 1.9.0 3.2.1