From aa369cfa6a0e75791bef6f3984139f8029d82956 Mon Sep 17 00:00:00 2001 From: fcombes Date: Thu, 14 Aug 2025 16:19:05 +0200 Subject: [PATCH 1/2] feat(basic): #11 allow parsing custom record as a formulary page request close #11 --- .gitignore | 1 + .../juery/basic/QueryStringParser.java | 13 ++++++ .../basic/parser/QueryStringParserImpl.java | 32 ++++++++++++++- .../juery/basic/ParserConfigurationTest.java | 1 + .../QueryStringParserImplDefaultTest.java | 40 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4a8e25e..1dadc5e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ target/ ### VS Code ### .vscode/ +.DS_Store diff --git a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/QueryStringParser.java b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/QueryStringParser.java index f823ecd..30c02f4 100644 --- a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/QueryStringParser.java +++ b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/QueryStringParser.java @@ -69,6 +69,19 @@ public interface QueryStringParser { */ PageRequest parse(String queryString); + /** + * Parse a Record form data object into {@link PageRequest}. + *

The record must contain only fields allow by the {@link ParserConfiguration}. + * Others fields are considered as property filters.

+ *

The type of the fields must be correct according to the {@link ParserConfiguration} ({@link Integer} or {@link String}. + * The type for property filters are {@link String} or {@link java.util.Collection} of {@link String}

+ *

All accessibles methods without argument are considerate except {@link Record#toString()} and {@link Record#hashCode()} ()}

+ * + * @param form The form data as a record object + * @return The PageRequest + */ + PageRequest parse(R form); + /** * Parse querystring parameter into a {@link Criteria}. * diff --git a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java index 6fe5d22..a427ce2 100644 --- a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java +++ b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java @@ -16,13 +16,18 @@ import fr.ght1pc9kc.juery.basic.utils.TemporalUtils; import lombok.RequiredArgsConstructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -40,6 +45,7 @@ public final class QueryStringParserImpl implements QueryStringParser { private static final int PAGE_START_INDEX = 0; private static final QueryStringFilterVisitor CRITERIA_FORMATTER = new QueryStringFilterVisitor(); + private static final Set EXCLUDED_FILTER_PARAMETERS = Set.of("equals", "toString", "hashCode", "getClass"); private final ParserConfiguration config; @@ -58,7 +64,7 @@ public String format(PageRequest pr) { if (!pr.filter().isEmpty()) { qs.append(pr.filter().accept(CRITERIA_FORMATTER)); } - if (qs.length() == 0) { + if (qs.isEmpty()) { return ""; } var c = qs.charAt(qs.length() - 1); @@ -106,6 +112,30 @@ public PageRequest parse(String queryString) { return parse(queryStringToMap(queryString)); } + public PageRequest parse(R form) { + Map> map = new HashMap<>(); + Method[] declaredMethods = form.getClass().getDeclaredMethods(); + + Stream.of(declaredMethods) + .filter(not(m -> EXCLUDED_FILTER_PARAMETERS.contains(m.getName()))) + .filter(m -> m.canAccess(form) && m.getParameterCount() == 0) + .forEach(m -> { + try { + Optional.ofNullable(m.invoke(form)) + .ifPresent(v -> { + if (v instanceof Collection collValue) { + map.put(m.getName(), collValue.stream().map(Object::toString).toList()); + } else { + map.put(m.getName(), List.of(v.toString())); + } + }); + } catch (InvocationTargetException | IllegalAccessException e) { + // do nothing + } + }); + return parse(map); + } + @Override public Criteria parseCriterionParameter(String key, List paramValue) { if (paramValue == null || paramValue.isEmpty()) { diff --git a/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/ParserConfigurationTest.java b/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/ParserConfigurationTest.java index 763a2f9..6c1cb5c 100644 --- a/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/ParserConfigurationTest.java +++ b/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/ParserConfigurationTest.java @@ -18,6 +18,7 @@ void should_create_default_configuration() { Assertions.assertThat(actual.excludeFilterParameters()).isEqualTo(Set.of( "_p", "_pp", "_s", "_from", "_to" )); + } @Test diff --git a/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImplDefaultTest.java b/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImplDefaultTest.java index b3542d0..5167c65 100644 --- a/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImplDefaultTest.java +++ b/juery-basic/src/test/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImplDefaultTest.java @@ -8,11 +8,13 @@ import fr.ght1pc9kc.juery.api.pagination.Sort; import fr.ght1pc9kc.juery.basic.QueryStringParser; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -205,4 +207,42 @@ private static Stream should_format_page_request_to_query_string() { public static Set assertableQueryString(String queryString) { return Set.of(queryString.split("&")); } + + @Test + void should_parse_record_as_page_request() { + record MyRecord( + Integer _p, + Integer _pp, + Integer _from, + Integer _to, + String _s, + Collection _id, + String name + ) { + } + { + MyRecord sample = new MyRecord(1, 10, null, null, "name,-email", List.of("42", "24"), "^Obiwan"); + + Assertions.assertThat(tested.parse(sample)) + .isEqualTo(PageRequest.of( + Pagination.of(10, 10, Sort.of( + new Order(Direction.ASC, "name"), + new Order(Direction.DESC, "email"))), + Criteria.property("_id").in(42, 24) + .and(Criteria.property("name").startWith("Obiwan")) + )); + } + { + MyRecord sample = new MyRecord(null, null, 101, 151, "name,-email", List.of("42", "24"), "∋Obiwan"); + + Assertions.assertThat(tested.parse(sample)) + .isEqualTo(PageRequest.of( + Pagination.of(101, 50, Sort.of( + new Order(Direction.ASC, "name"), + new Order(Direction.DESC, "email"))), + Criteria.property("_id").in(42, 24) + .and(Criteria.property("name").contains("Obiwan")) + )); + } + } } \ No newline at end of file From 98cfdf3edc297e8af4b8c83c06057af3816f2912 Mon Sep 17 00:00:00 2001 From: Marthym Date: Thu, 14 Aug 2025 20:18:56 +0200 Subject: [PATCH 2/2] chore(ci): upgrade to java 21 --- .github/workflows/build.yml | 4 +- .github/workflows/javadoc.yml | 4 +- .tool-versions | 1 + juery-api/pom.xml | 2 +- juery-basic/pom.xml | 2 +- .../basic/parser/QueryStringParserImpl.java | 67 ++++++++++--------- juery-jooq/pom.xml | 2 +- juery-mongo/pom.xml | 2 +- pom.xml | 6 +- 9 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 .tool-versions diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4c3258..8f741b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: - uses: actions/checkout@v4.2.2 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4.7.1 with: - java-version: 17 + java-version: 21 distribution: 'temurin' - name: Cache Maven packages uses: actions/cache@v4.2.3 diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index 0cfcab1..f8a0f08 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -24,10 +24,10 @@ jobs: - name: Checkout the repo uses: actions/checkout@v4.2.2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4.7.1 with: - java-version: 17 + java-version: 21 distribution: 'temurin' - name: Cache Maven packages diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0308c10 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +java temurin-21.0.4+7.0.LTS diff --git a/juery-api/pom.xml b/juery-api/pom.xml index 8c8f1d8..69ba3ab 100644 --- a/juery-api/pom.xml +++ b/juery-api/pom.xml @@ -4,7 +4,7 @@ fr.ght1pc9kc juery - 1.4.5-SNAPSHOT + 1.5.0-SNAPSHOT juery-api diff --git a/juery-basic/pom.xml b/juery-basic/pom.xml index 52e8b70..4609ffb 100644 --- a/juery-basic/pom.xml +++ b/juery-basic/pom.xml @@ -4,7 +4,7 @@ fr.ght1pc9kc juery - 1.4.5-SNAPSHOT + 1.5.0-SNAPSHOT juery-basic diff --git a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java index a427ce2..090054c 100644 --- a/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java +++ b/juery-basic/src/main/java/fr/ght1pc9kc/juery/basic/parser/QueryStringParserImpl.java @@ -18,6 +18,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -118,7 +119,8 @@ public PageRequest parse(R form) { Stream.of(declaredMethods) .filter(not(m -> EXCLUDED_FILTER_PARAMETERS.contains(m.getName()))) - .filter(m -> m.canAccess(form) && m.getParameterCount() == 0) + .filter(m -> m.getParameterCount() == 0 && !Modifier.isStatic(m.getModifiers()) + && m.canAccess(form)) .forEach(m -> { try { Optional.ofNullable(m.invoke(form)) @@ -151,44 +153,43 @@ public Criteria parseCriterionParameter(String key, List paramValue) { return Criteria.property(key).in(filteredListValues); } - String strValue = paramValue.get(0); + String strValue = paramValue.getFirst(); // Parse operation BiFunction operation = CriterionProperty::eq; - Object typedValue = null; + Object typedValue; if (!StringUtils.isBlank(strValue)) { - switch (strValue.charAt(0)) { - case QS_START_CHAR: + typedValue = switch (strValue.charAt(0)) { + case QS_START_CHAR -> { operation = CriterionProperty::startWith; - typedValue = parseValueType(strValue.substring(1)); - break; - case QS_END_CHAR: + yield parseValueType(strValue.substring(1)); + } + case QS_END_CHAR -> { operation = CriterionProperty::endWith; - typedValue = parseValueType(strValue.substring(1)); - break; - case QS_CONTAINS_CHAR: + yield parseValueType(strValue.substring(1)); + } + case QS_CONTAINS_CHAR -> { operation = CriterionProperty::contains; - typedValue = strValue.substring(1); - break; - case QS_LT_CHAR: + yield strValue.substring(1); + } + case QS_LT_CHAR -> { operation = CriterionProperty::lt; - typedValue = parseValueType(strValue.substring(1)); - break; - case QS_GT_CHAR: + yield parseValueType(strValue.substring(1)); + } + case QS_GT_CHAR -> { operation = CriterionProperty::gt; - typedValue = parseValueType(strValue.substring(1)); - break; - case QS_LTE_CHAR: + yield parseValueType(strValue.substring(1)); + } + case QS_LTE_CHAR -> { operation = CriterionProperty::lte; - typedValue = parseValueType(strValue.substring(1)); - break; - case QS_GTE_CHAR: + yield parseValueType(strValue.substring(1)); + } + case QS_GTE_CHAR -> { operation = CriterionProperty::gte; - typedValue = parseValueType(strValue.substring(1)); - break; - default: - typedValue = parseValueType(strValue); - } + yield parseValueType(strValue.substring(1)); + } + default -> parseValueType(strValue); + }; } else { typedValue = parseValueType(strValue); } @@ -198,12 +199,12 @@ public Criteria parseCriterionParameter(String key, List paramValue) { private Pagination parsePaginationByPage(Map> queryString) { int page = Optional.ofNullable(queryString.get(config.pageParameter())) - .flatMap(l -> Optional.ofNullable(l.get(0))) + .flatMap(l -> Optional.ofNullable(l.getFirst())) .map(Integer::parseInt) .orElse(0); int size = Optional.ofNullable(queryString.get(config.sizeParameter())) - .flatMap(l -> Optional.ofNullable(l.get(0))) + .flatMap(l -> Optional.ofNullable(l.getFirst())) .map(Integer::parseInt) .map(i -> Math.min(i, config.maxPageSize())) .orElse(config.maxPageSize()); @@ -217,18 +218,18 @@ private Pagination parsePaginationByPage(Map> queryString) private Pagination parsePaginationByOffset(Map> queryString) { int offset = Optional.ofNullable(queryString.get(config.fromParameter())) - .flatMap(l -> Optional.ofNullable(l.get(0))) + .flatMap(l -> Optional.ofNullable(l.getFirst())) .map(Integer::parseInt) .orElse(PAGE_START_INDEX); int maxTo = offset + config.maxPageSize() - 1; int size = Optional.ofNullable(queryString.get(config.sizeParameter())) - .flatMap(l -> Optional.ofNullable(l.get(0))) + .flatMap(l -> Optional.ofNullable(l.getFirst())) .map(Integer::parseInt) .map(i -> Math.min(i, config.maxPageSize())) .orElseGet(() -> Optional.ofNullable(queryString.get(config.toParameter())) - .flatMap(l -> Optional.ofNullable(l.get(0))) + .flatMap(l -> Optional.ofNullable(l.getFirst())) .map(Integer::parseInt) .map(i -> Math.min(i, maxTo)) .filter(i -> i > offset) diff --git a/juery-jooq/pom.xml b/juery-jooq/pom.xml index 921eccb..911b7f0 100644 --- a/juery-jooq/pom.xml +++ b/juery-jooq/pom.xml @@ -4,7 +4,7 @@ fr.ght1pc9kc juery - 1.4.5-SNAPSHOT + 1.5.0-SNAPSHOT juery-jooq diff --git a/juery-mongo/pom.xml b/juery-mongo/pom.xml index 873cad8..bbebc8f 100644 --- a/juery-mongo/pom.xml +++ b/juery-mongo/pom.xml @@ -4,7 +4,7 @@ fr.ght1pc9kc juery - 1.4.5-SNAPSHOT + 1.5.0-SNAPSHOT juery-mongo diff --git a/pom.xml b/pom.xml index 693e464..7287021 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.ght1pc9kc juery - 1.4.5-SNAPSHOT + 1.5.0-SNAPSHOT pom Juery @@ -35,8 +35,8 @@ UTF-8 - 17 - 17 + 21 + 21 1.18.38 26.0.2