Skip to content

Commit

Permalink
feat: add utility method for search string like or equal predicate (#122
Browse files Browse the repository at this point in the history
)

* feat: add DAO optimistic lock exception

* feat: add utility for search query like or equal predicate
  • Loading branch information
andrejpetras authored Jan 19, 2024
1 parent 597f2fc commit ae0e1e6
Show file tree
Hide file tree
Showing 22 changed files with 380 additions and 31 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,15 @@ 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) |

### Migration from older version

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
Expand Down Expand Up @@ -97,15 +94,15 @@ Change it to:
</dependencies>
```

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 |
|-----------------------------------|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| `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`
Expand All @@ -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 ✨

Expand Down
12 changes: 12 additions & 0 deletions extensions/jpa/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
<artifactId>tkit-quarkus-context</artifactId>
<version>${project.version}</version>
</dependency>

<!-- TEST -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class ConstraintException extends DAOException {
*/
private static final String PARAMETER = "constraint";

private static final String NAME = "constraintName";

/**
* The default constructor.
*
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,13 @@
*/
public class QueryCriteriaUtil {

/**
* Custom query like characters.
*/
private static final Map<String, String> DEFAULT_LIKE_MAPPING_CHARACTERS = Map.of(
"*", "%",
"?", "_");

/**
* The default constructor.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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<String, Object> parameters) {
StringBuilder sb = new StringBuilder();
Expand Down Expand Up @@ -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<String, Object> parameters) {
StringBuilder sb = new StringBuilder();
Expand All @@ -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<Predicate> predicates, CriteriaBuilder criteriaBuilder,
Expression<String> 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<Predicate> predicates, CriteriaBuilder criteriaBuilder,
Expression<String> 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<String> 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<String> 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<String> column,
String searchString, final boolean caseInsensitive,
Function<String, String> replaceFunction,
Map<String, String> likeMapping) {

if (searchString == null || searchString.isBlank()) {
return null;
}

// replace function for special characters
if (replaceFunction != null) {
searchString = replaceFunction.apply(searchString);
}

// case insensitive
Expression<String> columnDefinition = column;
if (caseInsensitive) {
searchString = searchString.toLowerCase();
columnDefinition = criteriaBuilder.lower(column);
}

// check for like characters
boolean like = false;
if (likeMapping != null) {
for (Map.Entry<String, String> 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("_", "\\_");
}
}
Loading

0 comments on commit ae0e1e6

Please sign in to comment.