Skip to content

Commit

Permalink
feat: add utility for search query like or equal predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
andrejpetras committed Jan 19, 2024
1 parent 05b44aa commit 5a9c5c6
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 13 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 @@ -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("_", "\\_");
}
}
Original file line number Diff line number Diff line change
@@ -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<Predicate>) invocation -> {
result.equal = true;
result.searchString = invocation.getArgument(1);
return predicate;
});

Expression<String> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
});
}
Expand Down
2 changes: 2 additions & 0 deletions extensions/rest-dto/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

/**
Expand Down
Loading

0 comments on commit 5a9c5c6

Please sign in to comment.