Fix EntityMatcher case sentsitivity config (#2706)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-09-29 15:08:21 +03:00
committed by GitHub
parent 7e5984b3c9
commit e747d55a38
3 changed files with 67 additions and 27 deletions

View File

@@ -32,6 +32,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; 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.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; 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.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties; 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. * Ensures that server returns a not found response in case of empty controller ID.
*/ */
@Test @Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) @ExpectEvents({ @Expect(type = TargetCreatedEvent.class) })
void rootRsWithoutId() throws Exception { void rootRsWithoutId() throws Exception {
mvc.perform(get("/controller/v1/")) mvc.perform(get("/controller/v1/"))
.andDo(MockMvcResultPrinter.print()) .andDo(MockMvcResultPrinter.print())
@@ -218,12 +220,19 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest {
@Test @Test
@WithUser(principal = "knownpricipal") @WithUser(principal = "knownpricipal")
@ExpectEvents({ @ExpectEvents({
@Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetCreatedEvent.class, count = 2),
@Expect(type = TargetPollEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), // assign to group
@Expect(type = TargetPollEvent.class, count = 2),
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1), @Expect(type = TenantConfigurationCreatedEvent.class, count = 1),
@Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) })
void pollWithModifiedWithOverridesGlobalPollingTime() throws Exception { 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), withUser("controller", CONTROLLER_ROLE_ANONYMOUS),
() -> { () -> {
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711))
@@ -231,6 +240,12 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(content().contentType(MediaTypes.HAL_JSON))
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); .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; return null;
})); }));
} }

View File

@@ -38,13 +38,19 @@ import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison.Operator;
public class EntityMatcher { public class EntityMatcher {
private final Node root; 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.root = root;
this.ignoreCase = ignoreCase;
} }
public static EntityMatcher of(final Node root) { 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 <T> boolean match(final T t) { public <T> 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 @SuppressWarnings({"java:S3776", "java:S3358", "java:S1125", "java:S6541"}) // better readable this way
private static <T> boolean match(final T t, final Node node) { private <T> boolean match(final T t, final Node node) {
if (node instanceof Node.Comparison comparison) { if (node instanceof Node.Comparison comparison) {
final String[] split = comparison.getKey().split("\\.", 2); final String[] split = comparison.getKey().split("\\.", 2);
try { 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 // TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not
return false; return false;
} }
return compare( return compareIgnoreCaseAware(
fieldValue == null ? null : ((Map<?, ?>) fieldValue).get(split[1]), fieldValue == null ? null : ((Map<?, ?>) fieldValue).get(split[1]),
op, op,
map( map(
@@ -76,14 +82,14 @@ public class EntityMatcher {
final BiPredicate<Object, Operator> compare; final BiPredicate<Object, Operator> compare;
if (split.length == 1) { if (split.length == 1) {
value = map(comparison.getValue(), getReturnType(fieldGetter)); value = map(comparison.getValue(), getReturnType(fieldGetter));
compare = (e, operator) -> compare(e, operator, value); compare = (e, operator) -> compareIgnoreCaseAware(e, operator, value);
} else { } else {
final Getter valueGetter = getGetter( final Getter valueGetter = getGetter(
(Class<?>) ((ParameterizedType) fieldGetter.type()).getActualTypeArguments()[0], split[1]); (Class<?>) ((ParameterizedType) fieldGetter.type()).getActualTypeArguments()[0], split[1]);
value = map(comparison.getValue(), getReturnType(valueGetter)); value = map(comparison.getValue(), getReturnType(valueGetter));
compare = (e, operator) -> { compare = (e, operator) -> {
try { 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) { } catch (final IllegalAccessException | InvocationTargetException ex) {
throw new IllegalArgumentException(ex); throw new IllegalArgumentException(ex);
} }
@@ -100,7 +106,7 @@ public class EntityMatcher {
}; };
} else { } else {
if (split.length == 1) { if (split.length == 1) {
return compare(fieldValue, op, map(comparison.getValue(), getReturnType(fieldGetter))); return compareIgnoreCaseAware(fieldValue, op, map(comparison.getValue(), getReturnType(fieldGetter)));
} else { } else {
if (split[1].contains(".")) { if (split[1].contains(".")) {
// nested field access // nested field access
@@ -108,13 +114,13 @@ public class EntityMatcher {
final Getter nestedFieldGetter = getGetter(getReturnType(fieldGetter), nestedSplit[0]); final Getter nestedFieldGetter = getGetter(getReturnType(fieldGetter), nestedSplit[0]);
final Getter valueGetter = getGetter(getReturnType(nestedFieldGetter), nestedSplit[1]); final Getter valueGetter = getGetter(getReturnType(nestedFieldGetter), nestedSplit[1]);
final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.get(fieldValue); final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.get(fieldValue);
return compare( return compareIgnoreCaseAware(
nestedFieldValue == null ? null : valueGetter.get(nestedFieldValue), nestedFieldValue == null ? null : valueGetter.get(nestedFieldValue),
op, op,
map(comparison.getValue(), getReturnType(valueGetter))); map(comparison.getValue(), getReturnType(valueGetter)));
} else { } else {
final Getter valueGetter = getGetter(getReturnType(fieldGetter), split[1]); final Getter valueGetter = getGetter(getReturnType(fieldGetter), split[1]);
return compare( return compareIgnoreCaseAware(
fieldValue == null ? null : valueGetter.get(fieldValue), fieldValue == null ? null : valueGetter.get(fieldValue),
op, op,
map(comparison.getValue(), getReturnType(valueGetter))); 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 <T> Getter getGetter(final Class<T> t, final String fieldName) throws NoSuchMethodException { private static <T> Getter getGetter(final Class<T> t, final String fieldName) throws NoSuchMethodException {
final String[] parts = fieldName.split("\\."); final String[] parts = fieldName.split("\\.");
if (parts.length > 1) { if (parts.length > 1) {
@@ -216,22 +239,22 @@ public class EntityMatcher {
} }
} }
private static boolean compare(final Object o1, final Operator op, final Object o2) { private static boolean compare(final Object entityValue, final Operator op, final Object comparisonValue) {
if ((o1 == null || o2 == null) && // null is not comparable! if ((entityValue == null || comparisonValue == null) && // null is not comparable!
(op == GT || op == GTE || op == LT || op == LTE)) { (op == GT || op == GTE || op == LT || op == LTE)) {
return false; return false;
} }
return switch (op) { return switch (op) {
case EQ -> Objects.equals(o1, o2); case EQ -> Objects.equals(entityValue, comparisonValue);
case NE -> !Objects.equals(o1, o2); case NE -> !Objects.equals(entityValue, comparisonValue);
case GT -> compare(o1, o2) > 0; case GT -> compare(entityValue, comparisonValue) > 0;
case GTE -> compare(o1, o2) >= 0; case GTE -> compare(entityValue, comparisonValue) >= 0;
case LT -> compare(o1, o2) < 0; case LT -> compare(entityValue, comparisonValue) < 0;
case LTE -> compare(o1, o2) <= 0; case LTE -> compare(entityValue, comparisonValue) <= 0;
case IN -> in(o1, o2); case IN -> in(entityValue, comparisonValue);
case NOT_IN -> !in(o1, o2); case NOT_IN -> !in(entityValue, comparisonValue);
case LIKE -> like(o2, o1); case LIKE -> like(comparisonValue, entityValue);
case NOT_LIKE -> !like(o2, o1); case NOT_LIKE -> !like(comparisonValue, entityValue);
}; };
} }

View File

@@ -161,8 +161,10 @@ public class QLSupport {
} }
} }
@SuppressWarnings({ "java:S1117" }) // it is again ignoreCase
public <A extends Enum<A> & QueryField> EntityMatcher entityMatcher(final String query, final Class<A> queryFieldType) { public <A extends Enum<A> & QueryField> EntityMatcher entityMatcher(final String query, final Class<A> 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);
} }
/** /**