From e747d55a38b69f45fdcd4b7c53b8517c1716eb53 Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Mon, 29 Sep 2025 15:08:21 +0300 Subject: [PATCH] Fix EntityMatcher case sentsitivity config (#2706) Signed-off-by: Avgustin Marinov --- .../rest/resource/DdiRootControllerTest.java | 23 +++++-- .../repository/jpa/ql/EntityMatcher.java | 67 +++++++++++++------ .../hawkbit/repository/jpa/ql/QLSupport.java | 4 +- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index 95fc62064..bef2d0e7f 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -32,6 +32,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; @@ -59,6 +60,7 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; +import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; @@ -147,7 +149,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { * Ensures that server returns a not found response in case of empty controller ID. */ @Test - @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) }) void rootRsWithoutId() throws Exception { mvc.perform(get("/controller/v1/")) .andDo(MockMvcResultPrinter.print()) @@ -218,12 +220,19 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @WithUser(principal = "knownpricipal") @ExpectEvents({ - @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TargetCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 1), // assign to group + @Expect(type = TargetPollEvent.class, count = 2), @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) void pollWithModifiedWithOverridesGlobalPollingTime() throws Exception { - withPollingTime("00:02:00, controllerid == 4711 -> 00:01:00", () -> callAs( + SecurityContextSwitch.callAsPrivileged(() -> { + final Target target = testdataFactory.createTarget("not4711"); + targetManagement.assignTargetsWithGroup("Europe", List.of(target.getControllerId())); + return null; + }); + + withPollingTime("00:02:00, controllerid == 4711 -> 00:01:00, group == 'Europe' -> 00:05:05", () -> callAs( withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) @@ -231,6 +240,12 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); + + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "not4711")) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:05:05"))); return null; })); } diff --git a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java index 110facace..ca38b4ec4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java +++ b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java @@ -38,13 +38,19 @@ import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison.Operator; public class EntityMatcher { private final Node root; + private final boolean ignoreCase; - private EntityMatcher(final Node root) { + private EntityMatcher(final Node root, final boolean ignoreCase) { this.root = root; + this.ignoreCase = ignoreCase; } public static EntityMatcher of(final Node root) { - return new EntityMatcher(root); + return of(root, false); + } + + public static EntityMatcher of(final Node root, final boolean ignoreCase) { + return new EntityMatcher(root, ignoreCase); } public boolean match(final T t) { @@ -52,7 +58,7 @@ public class EntityMatcher { } @SuppressWarnings({"java:S3776", "java:S3358", "java:S1125", "java:S6541"}) // better readable this way - private static boolean match(final T t, final Node node) { + private boolean match(final T t, final Node node) { if (node instanceof Node.Comparison comparison) { final String[] split = comparison.getKey().split("\\.", 2); try { @@ -65,7 +71,7 @@ public class EntityMatcher { // TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not return false; } - return compare( + return compareIgnoreCaseAware( fieldValue == null ? null : ((Map) fieldValue).get(split[1]), op, map( @@ -76,14 +82,14 @@ public class EntityMatcher { final BiPredicate compare; if (split.length == 1) { value = map(comparison.getValue(), getReturnType(fieldGetter)); - compare = (e, operator) -> compare(e, operator, value); + compare = (e, operator) -> compareIgnoreCaseAware(e, operator, value); } else { final Getter valueGetter = getGetter( (Class) ((ParameterizedType) fieldGetter.type()).getActualTypeArguments()[0], split[1]); value = map(comparison.getValue(), getReturnType(valueGetter)); compare = (e, operator) -> { try { - return compare(map(e == null ? null : valueGetter.get(e), getReturnType(valueGetter)), operator, value); + return compareIgnoreCaseAware(map(e == null ? null : valueGetter.get(e), getReturnType(valueGetter)), operator, value); } catch (final IllegalAccessException | InvocationTargetException ex) { throw new IllegalArgumentException(ex); } @@ -100,7 +106,7 @@ public class EntityMatcher { }; } else { if (split.length == 1) { - return compare(fieldValue, op, map(comparison.getValue(), getReturnType(fieldGetter))); + return compareIgnoreCaseAware(fieldValue, op, map(comparison.getValue(), getReturnType(fieldGetter))); } else { if (split[1].contains(".")) { // nested field access @@ -108,13 +114,13 @@ public class EntityMatcher { final Getter nestedFieldGetter = getGetter(getReturnType(fieldGetter), nestedSplit[0]); final Getter valueGetter = getGetter(getReturnType(nestedFieldGetter), nestedSplit[1]); final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.get(fieldValue); - return compare( + return compareIgnoreCaseAware( nestedFieldValue == null ? null : valueGetter.get(nestedFieldValue), op, map(comparison.getValue(), getReturnType(valueGetter))); } else { final Getter valueGetter = getGetter(getReturnType(fieldGetter), split[1]); - return compare( + return compareIgnoreCaseAware( fieldValue == null ? null : valueGetter.get(fieldValue), op, map(comparison.getValue(), getReturnType(valueGetter))); @@ -134,7 +140,24 @@ public class EntityMatcher { } } - @SuppressWarnings("java:S3011") // java:S3011 uses reflection to private members antway + private boolean compareIgnoreCaseAware(final Object entityValue, final Operator op, final Object comparisonValue) { + return compare(ignoreCase(entityValue), op, ignoreCase(comparisonValue)); + } + private Object ignoreCase(final Object o) { + if (!ignoreCase || o == null) { + return o; + } + // if here - ignoreCase in true and we have non-null value + if (o instanceof String str) { + return str.toLowerCase(); + } else if (o instanceof Collection collection) { + return collection.stream().map(this::ignoreCase).toList(); + } else { + return o; + } + } + + @SuppressWarnings("java:S3011") // java:S3011 uses reflection to private members anyway private static Getter getGetter(final Class t, final String fieldName) throws NoSuchMethodException { final String[] parts = fieldName.split("\\."); if (parts.length > 1) { @@ -216,22 +239,22 @@ public class EntityMatcher { } } - private static boolean compare(final Object o1, final Operator op, final Object o2) { - if ((o1 == null || o2 == null) && // null is not comparable! + private static boolean compare(final Object entityValue, final Operator op, final Object comparisonValue) { + if ((entityValue == null || comparisonValue == null) && // null is not comparable! (op == GT || op == GTE || op == LT || op == LTE)) { return false; } return switch (op) { - case EQ -> Objects.equals(o1, o2); - case NE -> !Objects.equals(o1, o2); - case GT -> compare(o1, o2) > 0; - case GTE -> compare(o1, o2) >= 0; - case LT -> compare(o1, o2) < 0; - case LTE -> compare(o1, o2) <= 0; - case IN -> in(o1, o2); - case NOT_IN -> !in(o1, o2); - case LIKE -> like(o2, o1); - case NOT_LIKE -> !like(o2, o1); + case EQ -> Objects.equals(entityValue, comparisonValue); + case NE -> !Objects.equals(entityValue, comparisonValue); + case GT -> compare(entityValue, comparisonValue) > 0; + case GTE -> compare(entityValue, comparisonValue) >= 0; + case LT -> compare(entityValue, comparisonValue) < 0; + case LTE -> compare(entityValue, comparisonValue) <= 0; + case IN -> in(entityValue, comparisonValue); + case NOT_IN -> !in(entityValue, comparisonValue); + case LIKE -> like(comparisonValue, entityValue); + case NOT_LIKE -> !like(comparisonValue, entityValue); }; } diff --git a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java index f381037d7..9bb459882 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java +++ b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/QLSupport.java @@ -161,8 +161,10 @@ public class QLSupport { } } + @SuppressWarnings({ "java:S1117" }) // it is again ignoreCase public & QueryField> EntityMatcher entityMatcher(final String query, final Class queryFieldType) { - return EntityMatcher.of(parser.parse(caseInsensitiveDB || ignoreCase ? query.toLowerCase() : query, queryFieldType)); + final boolean ignoreCase = caseInsensitiveDB || this.ignoreCase; // sync with DB and case sensitivity requirements + return EntityMatcher.of(parser.parse(ignoreCase ? query.toLowerCase() : query, queryFieldType), ignoreCase); } /**