diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 715520143..c53fdb585 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -12,6 +12,11 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version - Define Java 21 as the minimum version to run JNoSQL +=== Added + +- Include support to Restriction interface + + == [1.1.8] - 2025-05-21 === Removed diff --git a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxy.java b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxy.java index 0102daf77..0e9050041 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxy.java +++ b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxy.java @@ -14,7 +14,7 @@ */ package org.eclipse.jnosql.mapping.core.query; -import jakarta.data.exceptions.MappingException; +import jakarta.data.restrict.Restriction; import jakarta.enterprise.inject.spi.CDI; import org.eclipse.jnosql.mapping.core.repository.ThrowingSupplier; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; @@ -139,6 +139,18 @@ public abstract class AbstractRepositoryProxy implements InvocationHandler */ protected abstract Object executeParameterBased(Object instance, Method method, Object[] params); + + /** + * Executes a delete operation based on the method and parameters, specifically for methods that + * involve a {@link jakarta.data.restrict.Restriction} as a parameter. + * + * @param instance The instance on which the method was invoked. + * @param method The method being invoked, representing the delete operation. + * @param params The parameters of the method, including the restriction. + * @return The result of the delete operation. + */ + protected abstract Object executeDeleteRestriction(Object instance, Method method, Object[] params); + @Override public Object invoke(Object instance, Method method, Object[] params) throws Throwable { @@ -169,8 +181,6 @@ public Object invoke(Object instance, Method method, Object[] params) throws Thr case DEFAULT_METHOD -> { return unwrapInvocationTargetException(() -> InvocationHandler.invokeDefault(instance, method, params)); } - case ORDER_BY -> - throw new MappingException("Eclipse JNoSQL has not support for method that has OrderBy annotation"); case QUERY -> { return unwrapInvocationTargetException(() -> executeQuery(instance, method, params)); } @@ -188,6 +198,9 @@ public Object invoke(Object instance, Method method, Object[] params) throws Thr return unwrapInvocationTargetException(() -> INSERT.invoke(new AnnotationOperation.Operation(method, params, repository()))); } case DELETE -> { + if(params.length > 0 && params[0] instanceof Restriction) { + return unwrapInvocationTargetException(() -> executeDeleteRestriction(instance, method, params)); + } return unwrapInvocationTargetException(() -> DELETE.invoke(new AnnotationOperation.Operation(method, params, repository()))); } case UPDATE -> { diff --git a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/RepositoryType.java b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/RepositoryType.java index 2754cf3b1..40c34b719 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/RepositoryType.java +++ b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/query/RepositoryType.java @@ -20,7 +20,6 @@ import jakarta.data.repository.Delete; import jakarta.data.repository.Find; import jakarta.data.repository.Insert; -import jakarta.data.repository.OrderBy; import jakarta.data.repository.Query; import jakarta.data.repository.Save; import jakarta.data.repository.Update; @@ -79,10 +78,6 @@ public enum RepositoryType { * The method that belongs to the interface using a custom repository. */ CUSTOM_REPOSITORY(""), - /** - * Method that has {@link jakarta.data.repository.OrderBy} annotation - */ - ORDER_BY(""), /** * Method that has {@link Query} annotation */ @@ -162,19 +157,19 @@ public static RepositoryType of(Method method, Class repositoryType) { if (method.getReturnType().equals(CursoredPage.class)) { return CURSOR_PAGINATION; } - String methodName = method.getName(); - if (FIND_ALL.keyword.equals(methodName)) { - return FIND_ALL; - } - if (method.getAnnotationsByType(OrderBy.class).length > 0) { - return method.getAnnotation(Find.class) == null? ORDER_BY: PARAMETER_BASED; - } + Predicate hasAnnotation = a -> method.getAnnotation(a.annotation) != null; if (OPERATION_ANNOTATIONS.stream().anyMatch(hasAnnotation)) { return OPERATION_ANNOTATIONS.stream() .filter(hasAnnotation) .findFirst().orElseThrow(); } + + String methodName = method.getName(); + if (FIND_ALL.keyword.equals(methodName)) { + return FIND_ALL; + } + return KEY_WORLD_METHODS.stream() .filter(k -> methodName.startsWith(k.keyword)) .findFirst() diff --git a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java index fa270a092..6c22a1ed2 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java +++ b/jnosql-mapping/jnosql-mapping-core/src/main/java/org/eclipse/jnosql/mapping/core/repository/SpecialParameters.java @@ -19,6 +19,7 @@ import jakarta.data.Order; import jakarta.data.page.PageRequest; import jakarta.data.Sort; +import jakarta.data.restrict.Restriction; import java.util.ArrayList; import java.util.Arrays; @@ -33,17 +34,20 @@ * to apply pagination and sorting to your queries dynamically. */ public final class SpecialParameters { - static final SpecialParameters EMPTY = new SpecialParameters(null, null, Collections.emptyList()); + static final SpecialParameters EMPTY = new SpecialParameters(null, null, Collections.emptyList(), null); private final PageRequest pageRequest; private final List> sorts; private final Limit limit; - private SpecialParameters(PageRequest pageRequest, Limit limit, List> sorts) { + private final Restriction restriction; + + private SpecialParameters(PageRequest pageRequest, Limit limit, List> sorts, Restriction restriction) { this.pageRequest = pageRequest; this.sorts = sorts; this.limit = limit; + this.restriction = restriction; } /** @@ -71,7 +75,7 @@ public List> sorts() { * @return when there is no sort and PageRequest */ public boolean isEmpty() { - return this.sorts.isEmpty() && pageRequest == null && limit == null; + return this.sorts.isEmpty() && pageRequest == null && limit == null && restriction == null; } /** @@ -89,7 +93,7 @@ public boolean isSortEmpty() { * @return true if only have {@link PageRequest} */ public boolean hasOnlySort() { - return pageRequest == null && !sorts.isEmpty(); + return pageRequest == null && !sorts.isEmpty() && restriction == null && limit == null; } /** @@ -101,28 +105,39 @@ public Optional limit() { return Optional.ofNullable(limit); } + /** + * Returns the Restriction instance or {@link Optional#empty()} + * + * @return the Restriction instance or {@link Optional#empty()} + */ + public Optional> restriction() { + return Optional.ofNullable(restriction); + } + @Override public boolean equals(Object o) { - if (this == o) { - return true; - } if (o == null || getClass() != o.getClass()) { return false; } SpecialParameters that = (SpecialParameters) o; - return Objects.equals(pageRequest, that.pageRequest) && Objects.equals(sorts, that.sorts); + return Objects.equals(pageRequest, that.pageRequest) + && Objects.equals(sorts, that.sorts) + && Objects.equals(limit, that.limit) + && Objects.equals(restriction, that.restriction); } @Override public int hashCode() { - return Objects.hash(pageRequest, sorts); + return Objects.hash(pageRequest, sorts, limit, restriction); } @Override public String toString() { return "SpecialParameters{" + - "PageRequest=" + pageRequest + + "pageRequest=" + pageRequest + ", sorts=" + sorts + + ", limit=" + limit + + ", restriction=" + restriction + '}'; } @@ -130,6 +145,7 @@ static SpecialParameters of(Object[] parameters, Function sortPa List> sorts = new ArrayList<>(); PageRequest pageRequest = null; Limit limit = null; + Restriction restriction = null; for (Object parameter : parameters) { if (parameter instanceof Sort sort) { sorts.add(mapper(sort, sortParser)); @@ -141,6 +157,8 @@ static SpecialParameters of(Object[] parameters, Function sortPa Arrays.stream(sortArray).map(s -> mapper(s, sortParser)).forEach(sorts::add); } else if (parameter instanceof PageRequest request) { pageRequest = request; + } else if (parameter instanceof Restriction restrictionParameter) { + restriction = restrictionParameter; }else { if (parameter instanceof Iterable iterable) { for (Object value : iterable) { @@ -151,7 +169,7 @@ static SpecialParameters of(Object[] parameters, Function sortPa } } } - return new SpecialParameters(pageRequest, limit, sorts); + return new SpecialParameters(pageRequest, limit, sorts, restriction); } /** @@ -189,6 +207,7 @@ public static boolean isSpecialParameter(Class parameter) { || Limit.class.isAssignableFrom(parameter) || Order.class.isAssignableFrom(parameter) || PageRequest.class.isAssignableFrom(parameter) + || Restriction.class.isAssignableFrom(parameter) || parameter.isArray() && Sort.class.isAssignableFrom(parameter.getComponentType()); } diff --git a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxyTest.java b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxyTest.java index 0537b3d4a..7e14be342 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxyTest.java +++ b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/AbstractRepositoryProxyTest.java @@ -14,6 +14,7 @@ */ package org.eclipse.jnosql.mapping.core.query; +import jakarta.data.restrict.Restriction; import org.junit.jupiter.api.Test; import java.lang.reflect.Method; @@ -90,4 +91,11 @@ void shouldInvokeThrowsMappingException() throws Throwable { assertThrows(UnsupportedOperationException.class, () -> proxy.invoke(proxy, method, new Object[]{})); } + @Test + void shouldExecuteDeleteRestriction() throws Throwable { + Method method = TestRepository.class.getMethod("delete", Restriction.class); + Object result = proxy.invoke(proxy, method, new Object[]{(Restriction) () -> null}); + assertEquals("executeDeleteRestriction", result); + } + } \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/RepositoryTypeTest.java b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/RepositoryTypeTest.java index 57d6828a0..953c076ec 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/RepositoryTypeTest.java +++ b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/RepositoryTypeTest.java @@ -26,6 +26,7 @@ import jakarta.data.repository.Query; import jakarta.data.repository.Save; import jakarta.data.repository.Update; +import jakarta.data.restrict.Restriction; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; import org.eclipse.jnosql.mapping.NoSQLRepository; @@ -44,6 +45,7 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; class RepositoryTypeTest { @@ -71,54 +73,54 @@ void shouldReturnDefaultAtPageableRepository(Method method) { @Test void shouldReturnObjectMethod() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.OBJECT_METHOD, RepositoryType.of(getMethod(Object.class, "equals"), CrudRepository.class)); - Assertions.assertEquals(RepositoryType.OBJECT_METHOD, RepositoryType.of(getMethod(Object.class, "hashCode"), CrudRepository.class)); + assertEquals(RepositoryType.OBJECT_METHOD, RepositoryType.of(getMethod(Object.class, "equals"), CrudRepository.class)); + assertEquals(RepositoryType.OBJECT_METHOD, RepositoryType.of(getMethod(Object.class, "hashCode"), CrudRepository.class)); } @Test void shouldReturnFindBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(DevRepository.class, "findByName"), CrudRepository.class)); + assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(DevRepository.class, "findByName"), CrudRepository.class)); } @Test void shouldReturnFindFirstBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(DevRepository.class, "findFirst10ByAge"), CrudRepository.class)); + assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(DevRepository.class, "findFirst10ByAge"), CrudRepository.class)); } @Test void shouldReturnSave() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.SAVE, RepositoryType.of(getMethod(DevRepository.class, "save"), DevRepository.class)); + assertEquals(RepositoryType.SAVE, RepositoryType.of(getMethod(DevRepository.class, "save"), DevRepository.class)); } @Test void shouldReturnInsert() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.INSERT, RepositoryType.of(getMethod(DevRepository.class, "insert"), DevRepository.class)); + assertEquals(RepositoryType.INSERT, RepositoryType.of(getMethod(DevRepository.class, "insert"), DevRepository.class)); } @Test void shouldReturnDelete() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.DELETE, RepositoryType.of(getMethod(DevRepository.class, "delete"), DevRepository.class)); + assertEquals(RepositoryType.DELETE, RepositoryType.of(getMethod(DevRepository.class, "delete"), DevRepository.class)); } @Test void shouldReturnUpdate() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.UPDATE, RepositoryType.of(getMethod(DevRepository.class, "update"), DevRepository.class)); + assertEquals(RepositoryType.UPDATE, RepositoryType.of(getMethod(DevRepository.class, "update"), DevRepository.class)); } @Test void shouldReturnDeleteBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.DELETE_BY, RepositoryType.of(getMethod(DevRepository.class, "deleteByName"), CrudRepository.class)); + assertEquals(RepositoryType.DELETE_BY, RepositoryType.of(getMethod(DevRepository.class, "deleteByName"), CrudRepository.class)); } @Test void shouldReturnFindAllBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.FIND_ALL, RepositoryType.of(getMethod(DevRepository.class, "findAll"), CrudRepository.class)); + assertEquals(RepositoryType.FIND_ALL, RepositoryType.of(getMethod(DevRepository.class, "findAll"), CrudRepository.class)); } @Test void shouldReturnJNoSQLQuery() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.QUERY, RepositoryType.of(getMethod(DevRepository.class, "query"), CrudRepository.class)); + assertEquals(RepositoryType.QUERY, RepositoryType.of(getMethod(DevRepository.class, "query"), CrudRepository.class)); } @Test @@ -129,42 +131,42 @@ void shouldReturnError() { @Test void shouldReturnParameterBased() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.PARAMETER_BASED, RepositoryType.of(getMethod(DevRepository.class, "find"), CrudRepository.class)); + assertEquals(RepositoryType.PARAMETER_BASED, RepositoryType.of(getMethod(DevRepository.class, "find"), CrudRepository.class)); } @Test void shouldReturnParameterBased2() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.PARAMETER_BASED, RepositoryType.of(getMethod(DevRepository.class, "find2"), CrudRepository.class)); + assertEquals(RepositoryType.PARAMETER_BASED, RepositoryType.of(getMethod(DevRepository.class, "find2"), CrudRepository.class)); } @Test void shouldReturnCountBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.COUNT_BY, RepositoryType.of(getMethod(DevRepository.class, "countByName"), CrudRepository.class)); + assertEquals(RepositoryType.COUNT_BY, RepositoryType.of(getMethod(DevRepository.class, "countByName"), CrudRepository.class)); } @Test void shouldReturnCountAll() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.COUNT_ALL, RepositoryType.of(getMethod(DevRepository.class, "countAll"), CrudRepository.class)); + assertEquals(RepositoryType.COUNT_ALL, RepositoryType.of(getMethod(DevRepository.class, "countAll"), CrudRepository.class)); } @Test void shouldReturnExistsBy() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.EXISTS_BY, RepositoryType.of(getMethod(DevRepository.class, "existsByName"), CrudRepository.class)); + assertEquals(RepositoryType.EXISTS_BY, RepositoryType.of(getMethod(DevRepository.class, "existsByName"), CrudRepository.class)); } @Test void shouldReturnOrder() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.ORDER_BY, RepositoryType.of(getMethod(DevRepository.class, - "order"), CrudRepository.class)); - Assertions.assertEquals(RepositoryType.ORDER_BY, RepositoryType.of(getMethod(DevRepository.class, - "order2"), CrudRepository.class)); + Assertions.assertThrows(UnsupportedOperationException.class, + () -> RepositoryType.of(getMethod(DevRepository.class, "order"), CrudRepository.class)); + Assertions.assertThrows(UnsupportedOperationException.class, + () -> RepositoryType.of(getMethod(DevRepository.class, "order2"), CrudRepository.class)); } @Test void shouldDefaultMethod() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.DEFAULT_METHOD, RepositoryType.of(getMethod(DevRepository.class, + assertEquals(RepositoryType.DEFAULT_METHOD, RepositoryType.of(getMethod(DevRepository.class, "duplicate"), CrudRepository.class)); } @@ -176,7 +178,7 @@ void shouldReturnCustom() throws NoSuchMethodException { Mockito.when(instance.isResolvable()).thenReturn(true); cdi.when(CDI::current).thenReturn(current); Mockito.when(current.select(Calculate.class)).thenReturn(instance); - Assertions.assertEquals(RepositoryType.CUSTOM_REPOSITORY, RepositoryType.of(getMethod(Calculate.class, + assertEquals(RepositoryType.CUSTOM_REPOSITORY, RepositoryType.of(getMethod(Calculate.class, "sum"), CrudRepository.class)); } } @@ -189,7 +191,7 @@ void shouldReturnFindByCustom() throws NoSuchMethodException { Mockito.when(instance.isResolvable()).thenReturn(true); cdi.when(CDI::current).thenReturn(current); Mockito.when(current.select(Calculate.class)).thenReturn(instance); - Assertions.assertEquals(RepositoryType.CUSTOM_REPOSITORY, RepositoryType.of(getMethod(Calculate.class, + assertEquals(RepositoryType.CUSTOM_REPOSITORY, RepositoryType.of(getMethod(Calculate.class, "findBySum"), CrudRepository.class)); } } @@ -202,16 +204,29 @@ void shouldReturnFindByCustom2() throws NoSuchMethodException { Mockito.when(instance.isResolvable()).thenReturn(true); cdi.when(CDI::current).thenReturn(current); Mockito.when(current.select(Calculate.class)).thenReturn(instance); - Assertions.assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(Calculate.class, + assertEquals(RepositoryType.FIND_BY, RepositoryType.of(getMethod(Calculate.class, "findBySum"), Calculate.class)); } } @Test void shouldReturnFindByNameOrderByName() throws NoSuchMethodException { - Assertions.assertEquals(RepositoryType.CURSOR_PAGINATION, RepositoryType.of(getMethod(DevRepository.class, "findByNameOrderByName"), CrudRepository.class)); + assertEquals(RepositoryType.CURSOR_PAGINATION, RepositoryType.of(getMethod(DevRepository.class, "findByNameOrderByName"), CrudRepository.class)); } + @Test + void shouldFindRestrictionWithFind() throws NoSuchMethodException { + assertEquals(RepositoryType.PARAMETER_BASED, + RepositoryType.of(getMethod(DevRepository.class, "findRestriction"), CrudRepository.class)); + } + + @Test + void shouldFindRestrictionWithQuery() throws NoSuchMethodException { + assertEquals(RepositoryType.QUERY, + RepositoryType.of(getMethod(DevRepository.class, "queryRestriction"), CrudRepository.class)); + } + + private Method getMethod(Class repository, String methodName) throws NoSuchMethodException { return Stream.of(repository.getDeclaredMethods()) .filter(m -> m.getName().equals(methodName)) @@ -271,6 +286,15 @@ default int duplicate(int value) { List find2(String name); CursoredPage findByNameOrderByName(String name, PageRequest pageable); + List restriction(Restriction filter); + + @Find + @OrderBy("name") + List findRestriction(String name, Restriction filter); + + @Query("WHERE name = ?1") + @OrderBy("name") + List queryRestriction(String name, Restriction filter); } interface Calculate { diff --git a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepository.java b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepository.java index aa26e97ae..4b4cfe386 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepository.java +++ b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepository.java @@ -15,8 +15,10 @@ package org.eclipse.jnosql.mapping.core.query; import jakarta.data.page.CursoredPage; +import jakarta.data.repository.Delete; import jakarta.data.repository.Find; import jakarta.data.repository.Query; +import jakarta.data.restrict.Restriction; import java.util.List; import java.util.UUID; @@ -37,4 +39,9 @@ public interface TestRepository { @Find List find(); + + List restriction(Restriction restriction); + + @Delete + List delete(Restriction restriction); } diff --git a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepositoryProxy.java b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepositoryProxy.java index 8ec0ccfc0..a1b61b73e 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepositoryProxy.java +++ b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/query/TestRepositoryProxy.java @@ -76,4 +76,9 @@ protected Object executeCursorPagination(Object instance, Method method, Object[ protected Object executeParameterBased(Object instance, Method method, Object[] params) { return "executeParameterBased"; } + + @Override + protected Object executeDeleteRestriction(Object instance, Method method, Object[] params) { + return "executeDeleteRestriction"; + } } \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/repository/SpecialParametersTest.java b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/repository/SpecialParametersTest.java index 1ca34b8a7..ca5b6f90c 100644 --- a/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/repository/SpecialParametersTest.java +++ b/jnosql-mapping/jnosql-mapping-core/src/test/java/org/eclipse/jnosql/mapping/core/repository/SpecialParametersTest.java @@ -18,6 +18,8 @@ import jakarta.data.Order; import jakarta.data.page.PageRequest; import jakarta.data.Sort; +import jakarta.data.restrict.Restriction; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -168,8 +170,42 @@ void shouldReturnIterableOrderMapper(){ Sort.desc("AGE")); } + @Test + void shouldReturnRestriction() { + Restriction restriction = () -> null; + SpecialParameters parameters = SpecialParameters.of(new Object[]{10, "Otavio", restriction}, SORT_MAPPER); + assertFalse(parameters.isEmpty()); + Optional> restrictionOptional = parameters.restriction(); + assertTrue(restrictionOptional.isPresent()); + Restriction restriction1 = restrictionOptional.orElseThrow(); + assertEquals(restriction, restriction1); + } + + @Test + void shouldCheckToString() { + Restriction restriction = () -> null; + SpecialParameters parameters = SpecialParameters.of(new Object[]{10, "Otavio", restriction}, SORT_MAPPER); + Assertions.assertNotNull(parameters.toString()); + } + + @Test + void shouldCheckEquals() { + Restriction restriction = () -> null; + SpecialParameters parameters = SpecialParameters.of(new Object[]{10, "Otavio", restriction}, SORT_MAPPER); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(parameters).isEqualTo(SpecialParameters.of(new Object[]{10, "Otavio", restriction}, SORT_MAPPER)); + soft.assertThat(parameters).isNotEqualTo(null); + soft.assertThat(parameters).isNotEqualTo(new Object()); + soft.assertThat(parameters).isNotEqualTo(SpecialParameters.of(new Object[]{10, "Otavio"}, SORT_MAPPER)); + soft.assertThat(parameters).isNotEqualTo(SpecialParameters.of(new Object[]{10, "Otavio", Sort.asc("name")}, SORT_MAPPER)); + soft.assertThat(parameters.hashCode()).isEqualTo(SpecialParameters.of(new Object[]{10, "Otavio", restriction}, SORT_MAPPER).hashCode()); + }); + } + + + @ParameterizedTest - @ValueSource(classes = {Sort.class, Limit.class, PageRequest.class, Order.class}) + @ValueSource(classes = {Sort.class, Limit.class, PageRequest.class, Order.class, Restriction.class}) void shouldReturnTrueSpecialParameter(Class type){ org.assertj.core.api.Assertions.assertThat(SpecialParameters.isSpecialParameter(type)).isTrue(); } diff --git a/jnosql-mapping/jnosql-mapping-key-value/src/main/java/org/eclipse/jnosql/mapping/keyvalue/query/AbstractKeyValueRepositoryProxy.java b/jnosql-mapping/jnosql-mapping-key-value/src/main/java/org/eclipse/jnosql/mapping/keyvalue/query/AbstractKeyValueRepositoryProxy.java index ace68e8de..dcba4064e 100644 --- a/jnosql-mapping/jnosql-mapping-key-value/src/main/java/org/eclipse/jnosql/mapping/keyvalue/query/AbstractKeyValueRepositoryProxy.java +++ b/jnosql-mapping/jnosql-mapping-key-value/src/main/java/org/eclipse/jnosql/mapping/keyvalue/query/AbstractKeyValueRepositoryProxy.java @@ -69,4 +69,9 @@ protected Object executeParameterBased(Object instance, Method method, Object[] protected Object executeCursorPagination(Object instance, Method method, Object[] params) { throw new UnsupportedOperationException("Key Value repository does not support Cursor Pagination"); } + + @Override + protected Object executeDeleteRestriction(Object instance, Method method, Object[] params) { + throw new UnsupportedOperationException("Key Value repository does not support query method"); + } } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MappingDeleteQuery.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MappingDeleteQuery.java index a1ba3c852..e4e040ca4 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MappingDeleteQuery.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/MappingDeleteQuery.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Optional; -record MappingDeleteQuery(String entity, CriteriaCondition criteriaCondition) implements DeleteQuery { +public record MappingDeleteQuery(String entity, CriteriaCondition criteriaCondition) implements DeleteQuery { @Override diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java index 374da863b..1dadfaa99 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/AbstractSemiStructuredRepositoryProxy.java @@ -18,16 +18,22 @@ import jakarta.data.repository.Find; import jakarta.data.repository.OrderBy; import jakarta.data.repository.Query; +import jakarta.data.restrict.Restriction; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; import org.eclipse.jnosql.communication.semistructured.DeleteQuery; import org.eclipse.jnosql.communication.semistructured.QueryType; import org.eclipse.jnosql.mapping.core.repository.DynamicQueryMethodReturn; import org.eclipse.jnosql.mapping.core.repository.DynamicReturn; import org.eclipse.jnosql.mapping.core.repository.RepositoryReflectionUtils; import org.eclipse.jnosql.mapping.metadata.EntityMetadata; +import org.eclipse.jnosql.mapping.semistructured.MappingDeleteQuery; +import org.eclipse.jnosql.mapping.semistructured.MappingQuery; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Stream; @@ -62,7 +68,19 @@ protected Object executeQuery(Object instance, Method method, Object[] params) { .pageRequest(pageRequest) .prepareConverter(textQuery -> { var prepare = (org.eclipse.jnosql.mapping.semistructured.PreparedStatement) template().prepare(textQuery, entity); - prepare.setSelectMapper(query -> updateQueryDynamically(params, query)); + List> sortsFromAnnotation = getSorts(method, entityMetadata()); + if (sortsFromAnnotation.isEmpty()) { + prepare.setSelectMapper(query -> updateQueryDynamically(params, query)); + } else { + prepare.setSelectMapper(query -> { + var selectQuery = updateQueryDynamically(params, query); + List> sorts = new ArrayList<>(selectQuery.sorts()); + sorts.addAll(sortsFromAnnotation); + return new MappingQuery(sorts, selectQuery.limit(), selectQuery.skip(), + selectQuery.condition().orElse(null) + , entity); + }); + } return prepare; }).build(); return methodReturn.execute(); @@ -139,6 +157,29 @@ protected Object executeParameterBased(Object instance, Method method, Object[] return executeFindByQuery(method, params, type, updateQueryDynamically(params, query)); } + @Override + protected Object executeDeleteRestriction(Object instance, Method method, Object[] params) { + LOGGER.finest("Executing delete restriction on method: " + method); + Restriction restriction = restriction(params); + var entity = entityMetadata().name(); + Optional condition = RestrictionConverter.INSTANCE.parser(restriction, entityMetadata(), converters()); + var deleteQuery = new MappingDeleteQuery(entity, condition.orElse(null)); + this.template().delete(deleteQuery); + return Void.class; + } + + private Restriction restriction(Object[] params) { + if (params.length == 0) { + throw new IllegalArgumentException("The method must have at least one parameter for restriction"); + } + if (params[0] instanceof Restriction restriction) { + return restriction; + } else { + throw new IllegalArgumentException("The first parameter must be a Restriction, but was: " + params[0].getClass().getName()); + } + } + + private static List> getSorts(Method method, EntityMetadata metadata) { return Stream.of(method.getAnnotationsByType(OrderBy.class)) .map(order -> { diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/BaseSemiStructuredRepository.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/BaseSemiStructuredRepository.java index 37552daeb..eac51477f 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/BaseSemiStructuredRepository.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/BaseSemiStructuredRepository.java @@ -19,13 +19,16 @@ import jakarta.data.Sort; import jakarta.data.page.Page; import jakarta.data.page.PageRequest; +import jakarta.data.restrict.Restriction; import org.eclipse.jnosql.communication.Params; import org.eclipse.jnosql.communication.query.method.DeleteMethodProvider; import org.eclipse.jnosql.communication.query.method.SelectMethodProvider; import org.eclipse.jnosql.communication.semistructured.CommunicationObserverParser; import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; import org.eclipse.jnosql.communication.semistructured.DeleteQueryParser; import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; import org.eclipse.jnosql.communication.semistructured.SelectQueryParser; import org.eclipse.jnosql.mapping.core.Converters; import org.eclipse.jnosql.mapping.core.NoSQLPage; @@ -87,8 +90,7 @@ public abstract class BaseSemiStructuredRepository extends AbstractReposit */ protected abstract SemiStructuredTemplate template(); - - protected org.eclipse.jnosql.communication.semistructured.SelectQuery query(Method method, Object[] args) { + protected SelectQuery query(Method method, Object[] args) { var provider = SelectMethodProvider.INSTANCE; var selectQuery = provider.apply(method, entityMetadata().name()); var queryParams = SELECT_PARSER.apply(selectQuery, parser()); @@ -102,7 +104,7 @@ private static Object[] args(Object[] args) { return args == null ? EMPTY_PARAM : args; } - protected org.eclipse.jnosql.communication.semistructured.DeleteQuery deleteQuery(Method method, Object[] args) { + protected DeleteQuery deleteQuery(Method method, Object[] args) { var deleteMethodFactory = DeleteMethodProvider.INSTANCE; var deleteQuery = deleteMethodFactory.apply(method, entityMetadata().name()); var queryParams = DELETE_PARSER.apply(deleteQuery, parser()); @@ -138,7 +140,7 @@ protected ParamsBinder paramsBinder() { } @SuppressWarnings("unchecked") - protected Object executeFindByQuery(Method method, Object[] args, Class typeClass, org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected Object executeFindByQuery(Method method, Object[] args, Class typeClass, SelectQuery query) { DynamicReturn dynamicReturn = DynamicReturn.builder() .classSource(typeClass) .methodSource(method) @@ -152,7 +154,7 @@ protected Object executeFindByQuery(Method method, Object[] args, Class typeC return dynamicReturn.execute(); } - private org.eclipse.jnosql.communication.semistructured.SelectQuery includeInheritance(org.eclipse.jnosql.communication.semistructured.SelectQuery query){ + private SelectQuery includeInheritance(SelectQuery query){ EntityMetadata metadata = this.entityMetadata(); if(metadata.inheritance().isPresent()){ InheritanceMetadata inheritanceMetadata = metadata.inheritance().orElseThrow(); @@ -170,72 +172,112 @@ private org.eclipse.jnosql.communication.semistructured.SelectQuery includeInher return query; } - protected Long executeCountByQuery(org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected Long executeCountByQuery(SelectQuery query) { return template().count(query); } - protected boolean executeExistsByQuery(org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected boolean executeExistsByQuery(SelectQuery query) { return template().exists(query); } - protected Function> getPage(org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected Function> getPage(SelectQuery query) { return p -> { Stream entities = template().select(query); return NoSQLPage.of(entities.toList(), p); }; } - protected Function> getSingleResult(org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected Function> getSingleResult(SelectQuery query) { return p -> template().singleResult(query); } - protected Function> streamPagination(org.eclipse.jnosql.communication.semistructured.SelectQuery query) { + protected Function> streamPagination(SelectQuery query) { return p -> template().select(query); } - protected org.eclipse.jnosql.communication.semistructured.SelectQuery updateQueryDynamically(Object[] args, - org.eclipse.jnosql.communication.semistructured.SelectQuery query) { - var documentQuery = includeInheritance(query); - SpecialParameters special = DynamicReturn.findSpecialParameters(args, sortParser()); + protected SelectQuery updateQueryDynamically(Object[] args, SelectQuery query) { + + var selectInheritance = includeInheritance(query); + var special = DynamicReturn.findSpecialParameters(args, sortParser()); if (special.isEmpty()) { - return documentQuery; + return selectInheritance; + } + + final SelectQuery selectQuery; + if (special.restriction().isPresent()) { + selectQuery = includeRestrictCondition(special, selectInheritance); + } else { + selectQuery = selectInheritance; } + Optional limit = special.limit(); if (special.hasOnlySort()) { List> sorts = new ArrayList<>(); - sorts.addAll(documentQuery.sorts()); + sorts.addAll(selectQuery.sorts()); sorts.addAll(special.sorts()); - long skip = limit.map(l -> l.startAt() - 1).orElse(documentQuery.skip()); - long max = limit.map(Limit::maxResults).orElse((int) documentQuery.limit()); + long skip = limit.map(l -> l.startAt() - 1).orElse(selectQuery.skip()); + long max = limit.map(Limit::maxResults).orElse((int) selectQuery.limit()); return new MappingQuery(sorts, max, skip, - documentQuery.condition().orElse(null), - documentQuery.name()); + selectQuery.condition().orElse(null), + selectQuery.name()); } if (limit.isPresent()) { - long skip = limit.map(l -> l.startAt() - 1).orElse(documentQuery.skip()); - long max = limit.map(Limit::maxResults).orElse((int) documentQuery.limit()); - return new MappingQuery(documentQuery.sorts(), max, + long skip = limit.map(l -> l.startAt() - 1).orElse(selectQuery.skip()); + long max = limit.map(Limit::maxResults).orElse((int) selectQuery.limit()); + final List> sorts = new ArrayList<>(); + sorts.addAll(selectQuery.sorts()); + sorts.addAll(special.sorts()); + return new MappingQuery(sorts, max, skip, - documentQuery.condition().orElse(null), - documentQuery.name()); + selectQuery.condition().orElse(null), + selectQuery.name()); } - return special.pageRequest().map(p -> { + return special.pageRequest().map(p -> { long size = p.size(); long skip = NoSQLPage.skip(p); - List> sorts = documentQuery.sorts(); + List> sorts = selectQuery.sorts(); if (!special.sorts().isEmpty()) { - sorts = new ArrayList<>(documentQuery.sorts()); + sorts = new ArrayList<>(selectQuery.sorts()); sorts.addAll(special.sorts()); } return new MappingQuery(sorts, size, skip, - documentQuery.condition().orElse(null), documentQuery.name()); - }).orElse(documentQuery); + selectQuery.condition().orElse(null), selectQuery.name()); + }).orElseGet(() -> { + if (!special.sorts().isEmpty()) { + List> sorts = new ArrayList<>(selectQuery.sorts()); + sorts.addAll(special.sorts()); + return new MappingQuery(sorts, selectQuery.limit(), selectQuery.skip(), + selectQuery.condition().orElse(null), selectQuery.name()); + } + return selectQuery; + }); + } + + private SelectQuery includeRestrictCondition(SpecialParameters special, SelectQuery selectQuery) { + Restriction restriction = special.restriction().orElseThrow(); + + CriteriaCondition conditionConverted = RestrictionConverter.INSTANCE.parser(restriction, + entityMetadata(), converters()).orElse(null); + SelectQuery updateQuery = selectQuery; + if (conditionConverted != null) { + var conditionOptional = selectQuery.condition(); + + if (conditionOptional.isPresent()) { + CriteriaCondition condition = conditionOptional.orElseThrow(); + updateQuery = new MappingQuery(selectQuery.sorts(), selectQuery.limit(), + selectQuery.skip(), condition.and(conditionConverted), selectQuery.name()); + } else { + updateQuery = new MappingQuery(selectQuery.sorts(), selectQuery.limit(), + selectQuery.skip(), conditionConverted, selectQuery.name()); + } + } + return updateQuery; } protected Function sortParser() { diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverter.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverter.java new file mode 100644 index 000000000..1f084b34e --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverter.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.query; + +import jakarta.data.constraint.Between; +import jakarta.data.constraint.Constraint; +import jakarta.data.constraint.EqualTo; +import jakarta.data.constraint.GreaterThan; +import jakarta.data.constraint.GreaterThanOrEqual; +import jakarta.data.constraint.In; +import jakarta.data.constraint.LessThan; +import jakarta.data.constraint.LessThanOrEqual; +import jakarta.data.constraint.Like; +import jakarta.data.constraint.NotBetween; +import jakarta.data.constraint.NotEqualTo; +import jakarta.data.constraint.NotIn; +import jakarta.data.constraint.NotLike; +import jakarta.data.constraint.NotNull; +import jakarta.data.constraint.Null; +import jakarta.data.metamodel.BasicAttribute; +import jakarta.data.restrict.BasicRestriction; +import jakarta.data.restrict.CompositeRestriction; +import jakarta.data.restrict.Restriction; +import org.eclipse.jnosql.communication.Value; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntityMetadata; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Logger; + +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.and; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.between; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.eq; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.gt; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.gte; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.in; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.like; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.lt; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.lte; +import static org.eclipse.jnosql.communication.semistructured.CriteriaCondition.or; + + +/** + * Converts a {@link jakarta.data.restrict.Restriction} into its internal representation + * for query construction. + *

+ * This utility is used internally by the query engine to transform domain-specific + * constraints into a format suitable for building executable queries. + *

+ * + * + * @see jakarta.data.restrict.Restriction + */ +public enum RestrictionConverter { + INSTANCE; + + private static final Logger LOGGER = Logger.getLogger(RestrictionConverter.class.getName()); + + /** + * Parses a {@link jakarta.data.restrict.Restriction} and attempts to convert it + * into a {@link CriteriaCondition} based on the provided entity metadata and value converters. + * + *

This method is used internally to bridge the constraint API and the query + * execution engine.

+ * + * @param restriction the user-defined query restriction + * @param entityMetadata metadata about the entity being queried, used to resolve attributes + * @param converters converters for translating Java types to query-native formats + * @return an {@code Optional} if conversion is possible; otherwise, an empty Optional + * @throws NullPointerException if any of the arguments are {@code null} + */ + public Optional parser(Restriction restriction, EntityMetadata entityMetadata, Converters converters) { + Objects.requireNonNull(restriction, "restriction is required"); + Objects.requireNonNull(entityMetadata, "entityMetadata is required"); + Objects.requireNonNull(converters, "converters is required"); + + LOGGER.fine(() -> "Converter is invoked for restriction " + restriction); + + CriteriaCondition criteriaCondition; + switch (restriction) { + case BasicRestriction basicRestriction -> { + if (basicRestriction.expression() instanceof BasicAttribute basicAttribute) { + Constraint constraint = basicRestriction.constraint(); + criteriaCondition = condition(basicAttribute, constraint, entityMetadata, converters); + } else { + throw new UnsupportedOperationException("The expression " + basicRestriction.expression() + " is not supported"); + } + } + case CompositeRestriction compositeRestriction -> { + var negated = compositeRestriction.isNegated(); + var conditions = compositeRestriction.restrictions() + .stream() + .filter(r -> r instanceof BasicRestriction) + .map(r -> negated ? r.negate() : r) + .map(r -> parser(r, entityMetadata, + converters)) + .filter(Optional::isPresent) + .map(Optional::get) + .toArray(CriteriaCondition[]::new); + criteriaCondition = switch (compositeRestriction.type()) { + case ALL -> negated ? or(conditions) : and(conditions); + case ANY -> negated ? and(conditions) : or(conditions); + }; + } + default -> + throw new UnsupportedOperationException("Unsupported restriction type: " + restriction.getClass().getName()); + } + return Optional.ofNullable(criteriaCondition); + } + + private CriteriaCondition condition(BasicAttribute basicAttribute, Constraint constraint, + EntityMetadata entityMetadata, Converters converters) { + var name = basicAttribute.name(); + var fieldMetadata = entityMetadata.fieldMapping(name); + var converter = fieldMetadata.stream().flatMap(f -> f.converter().stream()).findFirst(); + + switch (constraint) { + case EqualTo equalTo -> { + var value = ValueConverter.of(equalTo::expression, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return eq(name, value); + } + case NotEqualTo notEqualTo -> { + var value = ValueConverter.of(notEqualTo::expression, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return eq(name, value).negate(); + } + case LessThan lessThan -> { + var value = ValueConverter.of(lessThan::bound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return lt(name, value); + } + + case GreaterThan greaterThan -> { + var value = ValueConverter.of(greaterThan::bound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return gt(name, value); + } + + case GreaterThanOrEqual greaterThanOrEqual -> { + var value = ValueConverter.of(greaterThanOrEqual::bound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return gte(name, value); + } + + case LessThanOrEqual lesserThanOrEqual -> { + var value = ValueConverter.of(lesserThanOrEqual::bound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return lte(name, value); + } + + case Between between -> { + var lowerBound = ValueConverter.of(between::lowerBound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + var upperBound = ValueConverter.of(between::upperBound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return between(name, List.of(lowerBound, upperBound)); + } + + case NotBetween between -> { + var lowerBound = ValueConverter.of(between::lowerBound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + var upperBound = ValueConverter.of(between::upperBound, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return between(name, List.of(lowerBound, upperBound)).negate(); + } + + case Like like -> { + var value = ValueConverter.of(like::pattern, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return like(name, value); + } + + case NotLike like -> { + var value = ValueConverter.of(like::pattern, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null)); + return like(name, value).negate(); + } + + case Null isNull -> { + return eq(name, Value.ofNull()); + } + + case NotNull isNull -> { + return eq(name, Value.ofNull()).negate(); + } + + case In in -> { + var values = in.expressions().stream().map(expression -> ValueConverter.of(() -> expression, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null))).toList(); + return in(name, values); + } + + case NotIn in -> { + var values = in.expressions().stream().map(expression -> ValueConverter.of(() -> expression, basicAttribute, converters, + converter.orElse(null), fieldMetadata.orElse(null))).toList(); + return in(name, values).negate(); + } + + default -> throw new UnsupportedOperationException("Unexpected value: " + constraint); + } + + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/ValueConverter.java b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/ValueConverter.java new file mode 100644 index 000000000..a7eb139a4 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/main/java/org/eclipse/jnosql/mapping/semistructured/query/ValueConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.query; + +import jakarta.data.expression.Expression; +import jakarta.data.metamodel.BasicAttribute; +import jakarta.data.spi.expression.literal.Literal; +import jakarta.nosql.AttributeConverter; +import org.eclipse.jnosql.communication.Value; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.FieldMetadata; + +import java.util.function.Supplier; + +enum ValueConverter { + +INSTANCE; + + static Object of(Supplier> supplier, BasicAttribute basicAttribute, + Converters converters, + Class> converter, + FieldMetadata fieldMetadata) { + var expression = supplier.get(); + var literal = getLiteral(expression); + return getValue(basicAttribute, converters, literal, converter, fieldMetadata); + } + + private static Literal getLiteral(Expression expression) { + if(expression instanceof Literal literal) { + return literal; + } else { + throw new UnsupportedOperationException("Currently only Literal values are supported for EqualTo constraints, but got: " + expression); + } + } + + private static Object getValue(BasicAttribute basicAttribute, Converters converters, + Literal literal, + Class> converter, + FieldMetadata fieldMetadata) { + if (converter != null) { + var attributeConverter = converters.get(fieldMetadata); + return attributeConverter.convertToDatabaseColumn(literal.value()); + } else { + return Value.of(literal.value()).get(basicAttribute.attributeType()); + } + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/Product.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/Product.java new file mode 100644 index 000000000..c5581c0be --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/Product.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Otávio Santana and others + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.entities; + +import jakarta.nosql.Column; +import jakarta.nosql.Convert; +import jakarta.nosql.Entity; + +import java.math.BigDecimal; + +@Entity +public class Product { + + @Column + private String name; + + @Column + private BigDecimal price; + + @Column + private ProductType type; + + @Column + @Convert(MoneyConverter.class) + private Money amount; + + public enum ProductType { + ELECTRONICS, CLOTHING, FOOD, FURNITURE + } +} \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/_Product.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/_Product.java new file mode 100644 index 000000000..d981ebca4 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/entities/_Product.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Otávio Santana and others + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.entities; + +import jakarta.data.metamodel.BasicAttribute; +import jakarta.data.metamodel.NumericAttribute; +import jakarta.data.metamodel.StaticMetamodel; +import jakarta.data.metamodel.TextAttribute; + +import javax.annotation.processing.Generated; +import java.math.BigDecimal; + +//CHECKSTYLE:OFF +@StaticMetamodel(Product.class) +@Generated(value = "The StaticMetamodel of the class ProductA provider by Eclipse JNoSQL", date = "2025-06-09T09:16:59.979587") +public interface _Product { + + String NAME = "name"; + String PRICE = "price"; + String TYPE = "type"; + String AMOUNT = "amount"; + + TextAttribute name = TextAttribute.of(Product.class, NAME); + NumericAttribute price = NumericAttribute.of(Product.class, PRICE, java.math.BigDecimal.class); + BasicAttribute type = BasicAttribute.of(Product.class, TYPE, Product.ProductType.class); + BasicAttribute amount = BasicAttribute.of(Product.class, AMOUNT, Money.class); + +} +//CHECKSTYLE:ON \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyRestrictionTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyRestrictionTest.java new file mode 100644 index 000000000..f4d7fe4a1 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyRestrictionTest.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.query; + +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.By; +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.OrderBy; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; +import jakarta.data.restrict.Restriction; +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Condition; +import org.eclipse.jnosql.communication.TypeReference; +import org.eclipse.jnosql.communication.Value; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import org.eclipse.jnosql.mapping.NoSQLRepository; +import org.eclipse.jnosql.mapping.PreparedStatement; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.MockProducer; +import org.eclipse.jnosql.mapping.semistructured.SemiStructuredTemplate; +import org.eclipse.jnosql.mapping.semistructured.entities.Address; +import org.eclipse.jnosql.mapping.semistructured.entities.Person; +import org.eclipse.jnosql.mapping.semistructured.entities.Product; +import org.eclipse.jnosql.mapping.semistructured.entities.Vendor; +import org.eclipse.jnosql.mapping.semistructured.entities._Product; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.reflect.Proxy; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.jnosql.communication.Condition.AND; +import static org.eclipse.jnosql.communication.Condition.BETWEEN; +import static org.eclipse.jnosql.communication.Condition.EQUALS; +import static org.eclipse.jnosql.communication.Condition.GREATER_THAN; +import static org.eclipse.jnosql.communication.Condition.IN; +import static org.eclipse.jnosql.communication.Condition.LESSER_EQUALS_THAN; +import static org.eclipse.jnosql.communication.Condition.LESSER_THAN; +import static org.eclipse.jnosql.communication.Condition.LIKE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class}) +@AddPackages(MockProducer.class) +@AddPackages(Reflections.class) +@AddExtensions({ReflectionEntityMetadataExtension.class}) +class CrudRepositoryProxyRestrictionTest { + + private SemiStructuredTemplate template; + + @Inject + private EntitiesMetadata entities; + + @Inject + private Converters converters; + + private ProductRepository repository; + + + @BeforeEach + public void setUp() { + this.template = Mockito.mock(SemiStructuredTemplate.class); + + var productHandler = new SemiStructuredRepositoryProxy<>(template, + entities, ProductRepository.class, converters); + + + + repository = (ProductRepository) Proxy.newProxyInstance(ProductRepository.class.getClassLoader(), + new Class[]{ProductRepository.class}, + productHandler); + } + + + @Test + void shouldRestrict() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + var products = repository.restriction(_Product.name.equalTo("Mac")); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + }); + } + + @Test + void shouldRestrictPage() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + Page products = repository.restriction(_Product.name.equalTo("Mac"), PageRequest.ofSize(2)); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(products.pageRequest()).isEqualTo(PageRequest.ofSize(2)); + softly.assertThat(products.nextPageRequest()).isEqualTo(PageRequest.ofSize(2).page(2)); + }); + } + + @Test + void shouldRestrictSort() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restriction(_Product.name.equalTo("Mac"), _Product.name.asc()); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.name.asc()); + }); + } + @Test + void shouldRestrictOrder() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restriction(_Product.name.equalTo("Mac"), Order.by(_Product.name.asc(), _Product.price.asc())); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(2); + softly.assertThat(query.sorts()).contains(_Product.name.asc(), _Product.price.asc()); + }); + } + + + @Test + void shouldRestrictSortByAnnotation() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restrictionOrderByPriceAsc(_Product.name.equalTo("Mac")); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.price.asc()); + }); + } + + @SuppressWarnings("rawtypes") + @Test + void shouldHaveCursor() { + + CursoredPage mock = Mockito.mock(CursoredPage.class); + when(mock.content()).thenReturn(List.of(new Product())); + + when(template.selectCursor(any(SelectQuery.class), any(PageRequest.class))).thenReturn(mock); + + CursoredPage cursor = repository.cursor(_Product.name.equalTo("Mac"), PageRequest.ofSize(10)); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).selectCursor(captor.capture(), Mockito.any(PageRequest.class)); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + softly.assertThat(cursor.content()).isNotEmpty().isNotNull(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.price.asc()); + }); + } + + @Test + void shouldShouldCombineRestrictionWithFind() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + List products = repository.findAll("Mac", _Product.price.greaterThan(BigDecimal.TEN)); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(AND); + List conditions = condition.element().get(new TypeReference<>() { + }); + softly.assertThat(conditions).hasSize(2); + CriteriaCondition equals = conditions.get(0); + CriteriaCondition greaterThan = conditions.get(1); + softly.assertThat(equals.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(greaterThan.element()).isEqualTo(Element.of(_Product.PRICE,BigDecimal.TEN)); + softly.assertThat(equals.condition()).isEqualTo(EQUALS); + softly.assertThat(greaterThan.condition()).isEqualTo(GREATER_THAN); + softly.assertThat(products).hasSize(1); + }); + } + + @Test + void shouldShouldCombineRestrictionWithQuery() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + when(template.prepare(Mockito.anyString(), Mockito.anyString())) + .thenReturn(Mockito.mock(org.eclipse.jnosql.mapping.semistructured.PreparedStatement.class)); + List products = repository.query("Mac", _Product.price.greaterThan(BigDecimal.TEN)); + + } + + @Test + void shouldDelete() { + + repository.delete(_Product.price.greaterThan(BigDecimal.TEN)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteQuery.class); + verify(template).delete(captor.capture()); + + SoftAssertions.assertSoftly(softly -> { + DeleteQuery value = captor.getValue(); + softly.assertThat(value.name()).isEqualTo("Product"); + softly.assertThat(value.condition()).isPresent(); + CriteriaCondition condition = value.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(GREATER_THAN); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.PRICE, BigDecimal.TEN)); + }); + + } + + + public interface ProductRepository extends CrudRepository { + @Find + List restriction(Restriction restriction); + @Find + Page restriction(Restriction restriction, PageRequest pageRequest); + + @Find + List restriction(Restriction restriction, Sort order); + @Find + List restriction(Restriction restriction, Order order); + + @OrderBy(_Product.PRICE) + @Find + List restrictionOrderByPriceAsc(Restriction restriction); + + @OrderBy(_Product.PRICE) + @Find + CursoredPage cursor(Restriction restriction, PageRequest pageRequest); + + + @Find + List findAll(@By("name") String name, Restriction restriction); + + @Query("where name = :name") + List query(@Param("name") String name, Restriction restriction); + + @Delete + void delete(Restriction restriction); + } +} diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyTest.java index b5f79a3a6..f85d452c9 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CrudRepositoryProxyTest.java @@ -20,6 +20,7 @@ import jakarta.data.repository.Param; import jakarta.data.repository.Query; import jakarta.data.Sort; +import jakarta.data.restrict.Restriction; import jakarta.inject.Inject; import org.assertj.core.api.SoftAssertions; import org.eclipse.jnosql.mapping.NoSQLRepository; @@ -38,6 +39,7 @@ import org.eclipse.jnosql.mapping.semistructured.SemiStructuredTemplate; import org.eclipse.jnosql.mapping.semistructured.entities.Address; import org.eclipse.jnosql.mapping.semistructured.entities.Person; +import org.eclipse.jnosql.mapping.semistructured.entities.Product; import org.eclipse.jnosql.mapping.semistructured.entities.Vendor; import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; import org.eclipse.jnosql.mapping.reflection.Reflections; @@ -305,7 +307,6 @@ void shouldFindByAgeANDName() { ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); verify(template).select(captor.capture()); assertThat(persons).contains(ada); - } @Test @@ -896,4 +897,5 @@ public interface AddressRepository extends CrudRepository { List
findByZipCodeZipOrderByZipCodeZip(String zip); } + } diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerRestrictionTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerRestrictionTest.java new file mode 100644 index 000000000..fb89f0a69 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/CustomRepositoryHandlerRestrictionTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.query; + +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.OrderBy; +import jakarta.data.restrict.Restrict; +import jakarta.data.restrict.Restriction; +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Condition; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import org.eclipse.jnosql.mapping.PreparedStatement; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.MockProducer; +import org.eclipse.jnosql.mapping.semistructured.SemiStructuredTemplate; +import org.eclipse.jnosql.mapping.semistructured.entities.Person; +import org.eclipse.jnosql.mapping.semistructured.entities.Product; +import org.eclipse.jnosql.mapping.semistructured.entities.Task; +import org.eclipse.jnosql.mapping.semistructured.entities._Product; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static org.eclipse.jnosql.communication.Condition.EQUALS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class}) +@AddPackages(MockProducer.class) +@AddPackages(Reflections.class) +@AddExtensions({ReflectionEntityMetadataExtension.class}) +class CustomRepositoryHandlerRestrictionTest { + + @Inject + private EntitiesMetadata entitiesMetadata; + + private SemiStructuredTemplate template; + + @Inject + private Converters converters; + + private ProductRepository repository; + + + @BeforeEach + void setUp() { + template = Mockito.mock(SemiStructuredTemplate.class); + CustomRepositoryHandler customRepositoryHandlerForPeople = CustomRepositoryHandler.builder() + .entitiesMetadata(entitiesMetadata) + .template(template) + .customRepositoryType(ProductRepository.class) + .converters(converters).build(); + + repository = (ProductRepository) Proxy.newProxyInstance(ProductRepository.class.getClassLoader(), new Class[]{ProductRepository.class}, + customRepositoryHandlerForPeople); + } + + @Test + void shouldRestrict() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + var products = repository.restriction(_Product.name.equalTo("Mac")); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + }); + } + + @Test + void shouldRestrictPage() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + Page products = repository.restriction(_Product.name.equalTo("Mac"), PageRequest.ofSize(2)); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(products.pageRequest()).isEqualTo(PageRequest.ofSize(2)); + softly.assertThat(products.nextPageRequest()).isEqualTo(PageRequest.ofSize(2).page(2)); + }); + } + + @Test + void shouldRestrictSort() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restriction(_Product.name.equalTo("Mac"), _Product.name.asc()); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.name.asc()); + }); + } + + @Test + void shouldRestrictOrder() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restriction(_Product.name.equalTo("Mac"), Order.by(_Product.name.asc(), _Product.price.asc())); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(2); + softly.assertThat(query.sorts()).contains(_Product.name.asc(), _Product.price.asc()); + }); + } + + + @Test + void shouldRestrictSortByAnnotation() { + + when(template.select(any(SelectQuery.class))) + .thenReturn(Stream.of(new Product())); + + repository.restrictionOrderByPriceAsc(_Product.name.equalTo("Mac")); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).select(captor.capture()); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.price.asc()); + }); + } + + @SuppressWarnings("rawtypes") + @Test + void shouldHaveCursor() { + + CursoredPage mock = Mockito.mock(CursoredPage.class); + when(mock.content()).thenReturn(List.of(new Product())); + + when(template.selectCursor(any(SelectQuery.class), any(PageRequest.class))).thenReturn(mock); + + CursoredPage cursor = repository.cursor(_Product.name.equalTo("Mac"), PageRequest.ofSize(10)); + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectQuery.class); + verify(template).selectCursor(captor.capture(), Mockito.any(PageRequest.class)); + SelectQuery query = captor.getValue(); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(query.name()).isEqualTo("Product"); + softly.assertThat(query.condition()).isPresent(); + softly.assertThat(cursor.content()).isNotEmpty().isNotNull(); + CriteriaCondition condition = query.condition().orElseThrow(); + softly.assertThat(condition).isInstanceOf(CriteriaCondition.class); + softly.assertThat(condition.condition()).isEqualTo(EQUALS); + softly.assertThat(condition.element()).isEqualTo(Element.of(_Product.NAME, "Mac")); + softly.assertThat(query.sorts()).hasSize(1); + softly.assertThat(query.sorts()).contains( _Product.price.asc()); + }); + } + + public interface ProductRepository { + @Find + List restriction(Restriction restriction); + @Find + Page restriction(Restriction restriction, PageRequest pageRequest); + + @Find + List restriction(Restriction restriction, Sort order); + @Find + List restriction(Restriction restriction, Order order); + + @OrderBy(_Product.PRICE) + @Find + List restrictionOrderByPriceAsc(Restriction restriction); + + @OrderBy(_Product.PRICE) + @Find + CursoredPage cursor(Restriction restriction, PageRequest pageRequest); + } +} \ No newline at end of file diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RepositoryProxyTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RepositoryProxyTest.java index 84e30f773..e91403957 100644 --- a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RepositoryProxyTest.java +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RepositoryProxyTest.java @@ -495,14 +495,14 @@ void shouldFindByNameLike() { @Test void shouldGotOrderException() { - Assertions.assertThrows(MappingException.class, () -> - personRepository.findBy()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> + personRepository.invalid()); } @Test void shouldGotOrderException2() { - Assertions.assertThrows(MappingException.class, () -> - personRepository.findByException()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> + personRepository.invalid2()); } @@ -1062,11 +1062,11 @@ default Map> partcionate(String name) { boolean existsByName(String name); @OrderBy("name") - List findBy(); + List invalid(); @OrderBy("name") @OrderBy("age") - List findByException(); + List invalid2(); @OrderBy("id") @Find diff --git a/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverterTest.java b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverterTest.java new file mode 100644 index 000000000..351cbefd1 --- /dev/null +++ b/jnosql-mapping/jnosql-mapping-semistructured/src/test/java/org/eclipse/jnosql/mapping/semistructured/query/RestrictionConverterTest.java @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Otavio Santana + */ +package org.eclipse.jnosql.mapping.semistructured.query; + +import jakarta.data.restrict.Restrict; +import jakarta.data.restrict.Restriction; +import jakarta.inject.Inject; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Condition; +import org.eclipse.jnosql.communication.TypeReference; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.metadata.EntityMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.MockProducer; +import org.eclipse.jnosql.mapping.semistructured.entities.Money; +import org.eclipse.jnosql.mapping.semistructured.entities.Product; +import org.eclipse.jnosql.mapping.semistructured.entities._Product; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class}) +@AddPackages(MockProducer.class) +@AddPackages(Reflections.class) +@AddExtensions({ReflectionEntityMetadataExtension.class}) +class RestrictionConverterTest { + + @Inject + private EntitiesMetadata entities; + + @Inject + private Converters converters; + + private EntityMetadata entityMetadata; + + @BeforeEach + void setUp() { + entityMetadata = entities.get(Product.class); + } + + + @Test + void shouldExecuteEqualsCondition() { + Restriction equalTo = _Product.name.equalTo("Macbook Pro"); + + Optional optional = RestrictionConverter.INSTANCE.parser(equalTo, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(element.name()).isEqualTo(_Product.NAME); + soft.assertThat(element.get()).isEqualTo("Macbook Pro"); + }); + } + + @Test + void shouldExecuteNotEqualsCondition() { + Restriction equalTo = _Product.name.equalTo("Macbook Pro").negate(); + + Optional optional = RestrictionConverter.INSTANCE.parser(equalTo, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + var equalsCondition = element.get(CriteriaCondition.class); + var equalsElement = equalsCondition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.NOT); + soft.assertThat(equalsCondition.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.get()).isEqualTo("Macbook Pro"); + }); + } + + @Test + void shouldExecuteLessThan() { + Restriction lessThan = _Product.price.lessThan(BigDecimal.TEN); + var optional = RestrictionConverter.INSTANCE.parser(lessThan, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.LESSER_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteNotLessEQuals() { + Restriction lessThanNegate = _Product.price.lessThan(BigDecimal.TEN).negate(); + var optional = RestrictionConverter.INSTANCE.parser(lessThanNegate, entityMetadata, converters); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.GREATER_EQUALS_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + + @Test + void shouldExecuteGreaterThan() { + Restriction greaterThan = _Product.price.greaterThan(BigDecimal.TEN); + var optional = RestrictionConverter.INSTANCE.parser(greaterThan, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.GREATER_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteNotGreaterEQuals() { + Restriction greaterThanNegate = _Product.price.greaterThan(BigDecimal.TEN).negate(); + var optional = RestrictionConverter.INSTANCE.parser(greaterThanNegate, entityMetadata, converters); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.LESSER_EQUALS_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteGreaterThanEquals() { + Restriction greaterThanEqual = _Product.price.greaterThanEqual(BigDecimal.TEN); + var optional = RestrictionConverter.INSTANCE.parser(greaterThanEqual, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.GREATER_EQUALS_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteNegateGreaterThanEquals() { + Restriction greaterThanEqualNegate = _Product.price.greaterThanEqual(BigDecimal.TEN).negate(); + var optional = RestrictionConverter.INSTANCE.parser(greaterThanEqualNegate, entityMetadata, converters); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.LESSER_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteLesserThanEquals() { + Restriction greaterThanEqual = _Product.price.lessThanEqual(BigDecimal.TEN); + var optional = RestrictionConverter.INSTANCE.parser(greaterThanEqual, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.LESSER_EQUALS_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteNegateLesserThanEquals() { + Restriction greaterThanEqualNegate = _Product.price.lessThanEqual(BigDecimal.TEN).negate(); + var optional = RestrictionConverter.INSTANCE.parser(greaterThanEqualNegate, entityMetadata, converters); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.GREATER_THAN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteBetween() { + Restriction between = _Product.price.between(BigDecimal.ZERO, BigDecimal.TEN); + var optional = RestrictionConverter.INSTANCE.parser(between, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.BETWEEN); + soft.assertThat(element.name()).isEqualTo(_Product.PRICE); + soft.assertThat(element.get()).isEqualTo(List.of(BigDecimal.ZERO, BigDecimal.TEN)); + }); + } + + @Test + void shouldExecuteNegateBetween() { + Restriction between = _Product.price.between(BigDecimal.ZERO, BigDecimal.TEN).negate(); + var optional = RestrictionConverter.INSTANCE.parser(between, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + var equalsCondition = element.get(CriteriaCondition.class); + var equalsElement = equalsCondition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.NOT); + soft.assertThat(equalsCondition.condition()).isEqualTo(Condition.BETWEEN); + soft.assertThat(equalsElement.name()).isEqualTo(_Product.PRICE); + soft.assertThat(equalsElement.get()).isEqualTo(List.of(BigDecimal.ZERO, BigDecimal.TEN)); + }); + } + + + @Test + void shouldExecuteLike() { + Restriction like = _Product.name.like("Macbook%"); + var optional = RestrictionConverter.INSTANCE.parser(like, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.LIKE); + soft.assertThat(element.name()).isEqualTo(_Product.NAME); + soft.assertThat(element.get()).isEqualTo("Macbook%"); + }); + } + + @Test + void shouldExecuteNegateLike() { + Restriction like = _Product.name.like("Macbook%").negate(); + var optional = RestrictionConverter.INSTANCE.parser(like, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + var equalsCondition = element.get(CriteriaCondition.class); + var equalsElement = equalsCondition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.NOT); + soft.assertThat(equalsCondition.condition()).isEqualTo(Condition.LIKE); + soft.assertThat(equalsElement.name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.get()).isEqualTo("Macbook%"); + }); + } + + @Test + void shouldExecuteNull() { + Restriction nullRestriction = _Product.name.isNull(); + var optional = RestrictionConverter.INSTANCE.parser(nullRestriction, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(element.name()).isEqualTo(_Product.NAME); + soft.assertThat(element.get()).isEqualTo(null); + }); + } + + @Test + void shouldExecuteNegateNull() { + + Restriction nullRestriction = _Product.name.isNull().negate(); + var optional = RestrictionConverter.INSTANCE.parser(nullRestriction, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + var equalsCondition = element.get(CriteriaCondition.class); + var equalsElement = equalsCondition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.NOT); + soft.assertThat(equalsCondition.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.get()).isEqualTo(null); + }); + } + + @Test + void shouldExecuteIn() { + Restriction in = _Product.name.in("Macbook Pro", "Macbook Air"); + var optional = RestrictionConverter.INSTANCE.parser(in, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.IN); + soft.assertThat(element.name()).isEqualTo(_Product.NAME); + soft.assertThat(element.get()).isEqualTo(List.of("Macbook Pro", "Macbook Air")); + }); + } + + @Test + void shouldExecuteNegateIn() { + + Restriction in = _Product.name.in("Macbook Pro", "Macbook Air").negate(); + var optional = RestrictionConverter.INSTANCE.parser(in, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + var equalsCondition = element.get(CriteriaCondition.class); + var equalsElement = equalsCondition.element(); + soft.assertThat(condition.condition()).isEqualTo(Condition.NOT); + soft.assertThat(equalsCondition.condition()).isEqualTo(Condition.IN); + soft.assertThat(equalsElement.name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.get()).isEqualTo(List.of("Macbook Pro", "Macbook Air")); + }); + } + + @Test + void shouldAll() { + Restriction all = Restrict.all(_Product.name.equalTo("Macbook Pro"), + _Product.price.greaterThan(BigDecimal.TEN)); + + var optional = RestrictionConverter.INSTANCE.parser(all, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + List conditions = element.get(new TypeReference<>() {}); + soft.assertThat(conditions).isNotEmpty().hasSize(2); + CriteriaCondition equalsElement = conditions.get(0); + soft.assertThat(condition.condition()).isEqualTo(Condition.AND); + soft.assertThat(equalsElement.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.element().name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.element().get()).isEqualTo("Macbook Pro"); + + CriteriaCondition greaterThan = conditions.get(1); + soft.assertThat(condition.condition()).isEqualTo(Condition.AND); + soft.assertThat(greaterThan.condition()).isEqualTo(Condition.GREATER_THAN); + soft.assertThat(greaterThan.element().name()).isEqualTo(_Product.PRICE); + soft.assertThat(greaterThan.element().get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldNegateAll() { + Restriction all = Restrict.all(_Product.name.equalTo("Macbook Pro"), + _Product.price.greaterThan(BigDecimal.TEN)).negate(); + + var optional = RestrictionConverter.INSTANCE.parser(all, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + List conditions = element.get(new TypeReference<>() {}); + soft.assertThat(conditions).isNotEmpty().hasSize(2); + CriteriaCondition equalsElement = conditions.get(0).element().get(CriteriaCondition.class); + soft.assertThat(condition.condition()).isEqualTo(Condition.OR); + soft.assertThat(equalsElement.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.element().name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.element().get()).isEqualTo("Macbook Pro"); + + CriteriaCondition greaterThan = conditions.get(1); + soft.assertThat(condition.condition()).isEqualTo(Condition.OR); + soft.assertThat(greaterThan.condition()).isEqualTo(Condition.LESSER_EQUALS_THAN); + soft.assertThat(greaterThan.element().name()).isEqualTo(_Product.PRICE); + soft.assertThat(greaterThan.element().get()).isEqualTo(BigDecimal.TEN); + }); + } + + + @Test + void shouldAny() { + Restriction any = Restrict.any(_Product.name.equalTo("Macbook Pro"), + _Product.price.greaterThan(BigDecimal.TEN)); + + var optional = RestrictionConverter.INSTANCE.parser(any, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + List conditions = element.get(new TypeReference<>() {}); + soft.assertThat(conditions).isNotEmpty().hasSize(2); + CriteriaCondition equalsElement = conditions.get(0); + soft.assertThat(condition.condition()).isEqualTo(Condition.OR); + soft.assertThat(equalsElement.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.element().name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.element().get()).isEqualTo("Macbook Pro"); + + CriteriaCondition greaterThan = conditions.get(1); + soft.assertThat(condition.condition()).isEqualTo(Condition.OR); + soft.assertThat(greaterThan.condition()).isEqualTo(Condition.GREATER_THAN); + soft.assertThat(greaterThan.element().name()).isEqualTo(_Product.PRICE); + soft.assertThat(greaterThan.element().get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldNegateAny() { + Restriction any = Restrict.any(_Product.name.equalTo("Macbook Pro"), + _Product.price.greaterThan(BigDecimal.TEN)).negate(); + + var optional = RestrictionConverter.INSTANCE.parser(any, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + List conditions = element.get(new TypeReference<>() {}); + soft.assertThat(conditions).isNotEmpty().hasSize(2); + CriteriaCondition equalsElement = conditions.get(0).element().get(CriteriaCondition.class); + soft.assertThat(condition.condition()).isEqualTo(Condition.AND); + soft.assertThat(equalsElement.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(equalsElement.element().name()).isEqualTo(_Product.NAME); + soft.assertThat(equalsElement.element().get()).isEqualTo("Macbook Pro"); + + CriteriaCondition greaterThan = conditions.get(1); + soft.assertThat(condition.condition()).isEqualTo(Condition.AND); + soft.assertThat(greaterThan.condition()).isEqualTo(Condition.LESSER_EQUALS_THAN); + soft.assertThat(greaterThan.element().name()).isEqualTo(_Product.PRICE); + soft.assertThat(greaterThan.element().get()).isEqualTo(BigDecimal.TEN); + }); + } + + @Test + void shouldExecuteEqualsConditionWithConverter() { + Restriction equalTo = _Product.amount.equalTo(new Money("USD", BigDecimal.valueOf(100))); + + Optional optional = RestrictionConverter.INSTANCE.parser(equalTo, entityMetadata, converters); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(optional).isPresent(); + var condition = optional.orElseThrow(); + var element = condition.element(); + + soft.assertThat(condition.condition()).isEqualTo(Condition.EQUALS); + soft.assertThat(element.name()).isEqualTo(_Product.AMOUNT); + soft.assertThat(element.get()).isInstanceOf(String.class); + soft.assertThat(element.get()).isEqualTo("USD 100"); + }); + } + + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index a3ba78b94..83bb97e8c 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 4.1.0 3.0.1 2.1.3 - 1.0.1 + 1.1.0-SNAPSHOT 1.0.0 3.1.1 3.11.1