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

@@ -620,6 +620,41 @@ public interface TargetManagement {
@PreAuthorize(HAS_AUTH_UPDATE_TARGET)
Target assignType(@NotEmpty String controllerId, @NotNull Long targetTypeId);
/**
* Finds targets by group or subgroup.
* @param group - provided group/subgroup to filter for
* @param withSubgroups - whether is a subgroup or not e.g. x/y/z
* @param pageable - page parameter
* @return all matching targets to provided group/subgroup
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
Page<Target> findTargetsByGroup(@NotEmpty String group, boolean withSubgroups, @NotNull Pageable pageable);
/**
* Finds all the distinct target groups in the scope of a tenant
*
* @return list of all distinct target groups
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
List<String> findGroups();
/**
* Assigns the target group of the targets matching the provided rsql filter.
*
* @param group target group parameter
* @param rsql rsql filter for {@link Target}
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET)
void assignTargetGroupWithRsql(String group, @NotNull String rsql);
/**
* Assigns the provided group to the targets which are in the provided list of controllerIds.
*
* @param group target group parameter
* @param controllerIds list of targets
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET)
void assignTargetsWithGroup(String group, @NotEmpty List<String> controllerIds);
/**
* updates the {@link Target}.
*

View File

@@ -73,6 +73,12 @@ public interface TargetCreate {
*/
TargetCreate status(@NotNull TargetUpdateStatus status);
/**
* @param group for setting the group of the target
* @return updated builder instance
*/
TargetCreate group(String group);
/**
* @return peek on current state of {@link Target} in the builder
*/

View File

@@ -70,4 +70,11 @@ public interface TargetUpdate {
* @return updated builder instance
*/
TargetUpdate requestAttributes(Boolean requestAttributes);
/**
*
* @param group for {@link Target#getGroup()}
* @return updated builder instance
*/
TargetUpdate group(String group);
}

View File

@@ -124,4 +124,10 @@ public interface Target extends NamedEntity {
* {@link #getControllerAttributes()}.
*/
boolean isRequestControllerAttributes();
/**
*
* @return the group of the target
*/
String getGroup();
}

View File

@@ -34,6 +34,7 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
protected TargetUpdateStatus status;
protected Boolean requestAttributes;
protected Long targetTypeId;
protected String group;
protected AbstractTargetUpdateCreate(final String controllerId) {
this.controllerId = AbstractBaseEntityBuilder.strip(controllerId);
@@ -83,6 +84,11 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
return (T) this;
}
public T group(final String group) {
this.group = group;
return (T) this;
}
public String getControllerId() {
return controllerId;
}
@@ -107,4 +113,6 @@ public class AbstractTargetUpdateCreate<T> extends AbstractNamedEntityBuilder<T>
return targetTypeId;
}
public Optional<String> getGroup() { return Optional.ofNullable(group); }
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_target ADD COLUMN target_group VARCHAR(256);
CREATE INDEX sp_idx_target_group ON sp_target (tenant, target_group);

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".
*