Introduce target grouping (#2538)

* Introduce target grouping

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* minor refactor

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* throw validation exception instead direct returning bad request response

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fix group query parameter

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* remove wrongly added import

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* add review fixes

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* apply latest review changes

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* apply latest changes after sybnc/review

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

* fix after review

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>

---------

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>
This commit is contained in:
Stanislav Trailov
2025-07-14 15:48:37 +03:00
committed by GitHub
parent e7373275bf
commit b4793fcce1
23 changed files with 888 additions and 2 deletions

View File

@@ -53,6 +53,7 @@ public class JpaTargetCreate extends AbstractTargetUpdateCreate<TargetCreate> im
target.setAddress(address);
target.setUpdateStatus(getStatus().orElse(TargetUpdateStatus.UNKNOWN));
getLastTargetQuery().ifPresent(target::setLastTargetQuery);
target.setGroup(getGroup().orElse(null));
return target;
}

View File

@@ -28,7 +28,9 @@ import java.util.stream.Collectors;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.validation.constraints.NotEmpty;
@@ -562,6 +564,57 @@ public class JpaTargetManagement implements TargetManagement {
return targetRepository.save(target);
}
@Override
public Page<Target> findTargetsByGroup(String group, final boolean withSubgroups, final Pageable pageable) {
if (withSubgroups) {
// search for eq(group) and like(group%)
return JpaManagementHelper
.findAllWithCountBySpec(targetRepository, List.of(TargetSpecifications.eqOrSubTargetGroup(group)), pageable);
} else {
return JpaManagementHelper
.findAllWithCountBySpec(targetRepository, List.of(TargetSpecifications.eqTargetGroup(group)), pageable);
}
}
@Override
public List<String> findGroups() {
return targetRepository.findDistinctGroups();
}
@Override
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public void assignTargetGroupWithRsql(String group, String rsql) {
final Specification<JpaTarget> rsqlSpecification = RsqlUtility.getInstance().buildRsqlSpecification(rsql, TargetFields.class);
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaUpdate<JpaTarget> criteriaUpdateQuery = cb.createCriteriaUpdate(JpaTarget.class);
final Root<JpaTarget> root = 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);
entityManager.createQuery(criteriaUpdateQuery).executeUpdate();
}
@Override
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public void assignTargetsWithGroup(String group, List<String> controllerIds) {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaUpdate<JpaTarget> criteriaQuery = cb.createCriteriaUpdate(JpaTarget.class);
Root<JpaTarget> root = criteriaQuery.from(JpaTarget.class);
CriteriaBuilder.In<String> in = cb.in(root.get("controllerId"));
controllerIds.forEach(in::value);
entityManager.createQuery(criteriaQuery.set("group", group).where(in)).executeUpdate();
}
@Override
@Transactional
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,

View File

@@ -154,6 +154,11 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
// set default request controller attributes to true, because we want to request them the first time
private boolean requestControllerAttributes = true;
@Setter
@Getter
@Column(name = "target_group")
private String group;
// actually it is OneToOne - but lazy loading is not supported for OneToOne (at least for hibernate 6.6.2)
@OneToMany(fetch = FetchType.LAZY, mappedBy = "target", cascade = { CascadeType.ALL }, orphanRemoval = true)
@PrimaryKeyJoinColumn

View File

@@ -10,6 +10,7 @@
package org.eclipse.hawkbit.repository.jpa.repository;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import jakarta.persistence.EntityManager;
@@ -96,4 +97,11 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget> {
@Transactional
@Query("DELETE FROM JpaTarget t WHERE t.tenant = :tenant")
void deleteByTenant(@Param("tenant") String tenant);
/**
* Finds all available distinct target groups
* @return all target groups
*/
@Query(value = "SELECT DISTINCT target_group FROM sp_target WHERE target_group IS NOT NULL", nativeQuery = true)
List<String> findDistinctGroups();
}

View File

@@ -192,6 +192,22 @@ public final class TargetSpecifications {
};
}
public static Specification<JpaTarget> eqTargetGroup(final String targetGroup) {
return (targetRoot, query, criteriaBuilder) -> {
final String groupTextToLower = targetGroup.toLowerCase();
return criteriaBuilder.equal(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), groupTextToLower);
};
}
public static Specification<JpaTarget> eqOrSubTargetGroup(final String targetGroupSearch) {
return (targetRoot, query, criteriaBuilder) -> {
final String searchTextToLower = targetGroupSearch.toLowerCase();
return criteriaBuilder.or(
criteriaBuilder.equal(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), searchTextToLower),
criteriaBuilder.like(criteriaBuilder.lower(targetRoot.get(JpaTarget_.group)), searchTextToLower.concat("/%")));
};
}
/**
* {@link Specification} for retrieving {@link Target}s by "like controllerId".
*