diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResourceTest.java index f53ef26e6..b510e4161 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResourceTest.java @@ -22,6 +22,8 @@ import java.util.List; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetGroupRestApi; +import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.model.TargetTag; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; @@ -229,4 +231,30 @@ public class MgmtTargetGroupResourceTest extends AbstractManagementApiIntegratio .andExpect(jsonPath("content.[1].controllerId", Matchers.equalTo("target2"))) .andExpect(jsonPath("content.[2].controllerId", Matchers.equalTo("target3"))); } + + @Test + void shouldAssignGroupToTargetsFilteredByTagNotEqual() throws Exception { + targetManagement.create(builder().controllerId("target1").build()); + targetManagement.create(builder().controllerId("target2").build()); + targetManagement.create(builder().controllerId("target3").build()); + + final TargetTag tag1 = targetTagManagement.create(TargetTagManagement.Create.builder().name("tag1").build()); + final TargetTag tag2 = targetTagManagement.create(TargetTagManagement.Create.builder().name("tag2").build()); + + targetManagement.assignTag(List.of("target1"), tag1.getId()); + targetManagement.assignTag(List.of("target2"), tag1.getId()); + targetManagement.assignTag(List.of("target3"), tag2.getId()); + + mvc.perform(put(MgmtTargetGroupRestApi.TARGETGROUPS_V1 + "/FilteredGroup") + .contentType(MediaType.APPLICATION_JSON) + .param("q", "tag!=tag1")) + .andExpect(status().isNoContent()); + + mvc.perform(get(MgmtTargetGroupRestApi.TARGETGROUPS_V1 + "/FilteredGroup/assigned") + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("content", Matchers.hasSize(1))) + .andExpect(jsonPath("content.[0].controllerId", Matchers.equalTo("target3"))); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java index da2611a8d..a5d2f0e3b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java @@ -31,6 +31,7 @@ import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.criteria.MapJoin; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.MapAttribute; import jakarta.validation.constraints.NotEmpty; @@ -39,9 +40,11 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.jpa.Jpa; import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper; import org.eclipse.hawkbit.repository.jpa.acm.AccessController; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; +import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; @@ -336,11 +339,24 @@ public class JpaTargetManagement final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); final CriteriaUpdate criteriaUpdateQuery = cb.createCriteriaUpdate(JpaTarget.class); - final Root root = criteriaUpdateQuery.getRoot(); + final Root updateRoot = criteriaUpdateQuery.getRoot(); criteriaUpdateQuery.set("group", group); - // get predicate from rsql specification using a dummy query in order to execute batch update - final Predicate predicate = rsqlSpecification.toPredicate(root, entityManager.getCriteriaBuilder().createQuery(JpaTarget.class), cb); - criteriaUpdateQuery.where(predicate); + + if (Jpa.JPA_VENDOR == Jpa.JpaVendor.ECLIPSELINK) { + // EclipseLink: use subquery approach — applying predicate directly to the UPDATE root + // fails for NOT EXISTS due to UpdateAllQuery's @Id resolution bug + // BUG Reported: https://github.com/eclipse-ee4j/eclipselink/issues/2757 + final Subquery subquery = criteriaUpdateQuery.subquery(Long.class); + final Root subRoot = subquery.from(JpaTarget.class); + subquery.select(subRoot.get(AbstractJpaBaseEntity_.ID)); + subquery.where(rsqlSpecification.toPredicate(subRoot, cb.createQuery(JpaTarget.class), cb)); + criteriaUpdateQuery.where(updateRoot.get(AbstractJpaBaseEntity_.ID).in(subquery)); + } else { + // Hibernate: apply predicate directly to the UPDATE root — Hibernate handles + // NOT EXISTS subqueries correctly in CriteriaUpdate context + final Predicate predicate = rsqlSpecification.toPredicate(updateRoot, cb.createQuery(JpaTarget.class), cb); + criteriaUpdateQuery.where(predicate); + } entityManager.createQuery(criteriaUpdateQuery).executeUpdate(); }