* add `min artifacts` requirement on the Software Module Type level for Software Module completeness * removed `complete` Distribution Set property from DB - calculated runtime * Distribution Set and Software Module completeness is calcualted on demand in memory (TODO: implement cache) * locking of Software Module now requires the software module to be `completed` * removed 'complete' search field for DistributionSet type. Still keep (DEPRECATED) limited support for search with 'complete' - only on the first level of expression and with AND. I.e. complete==true, complete==false and id=in=(1, 3) is suppoted, while complete==false or id=in=(1, 3) and id=in(1, 3) and (type==os and complete==true) are not Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -13,18 +13,21 @@ import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_D
|
||||
import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX;
|
||||
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.IMPLICIT_LOCK_ENABLED;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetFields;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
|
||||
@@ -36,17 +39,20 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
|
||||
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetTag;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.Node;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTagRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
||||
import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetSpecification;
|
||||
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
@@ -58,7 +64,9 @@ import org.eclipse.hawkbit.utils.TenantConfigHelper;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -67,8 +75,10 @@ import org.springframework.util.ObjectUtils;
|
||||
|
||||
@Service
|
||||
@ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "distribution-set-management" }, matchIfMissing = true)
|
||||
@Slf4j
|
||||
public class JpaDistributionSetManagement
|
||||
extends AbstractJpaRepositoryWithMetadataManagement<JpaDistributionSet, DistributionSetManagement.Create, DistributionSetManagement.Update, DistributionSetRepository, DistributionSetFields, String, String>
|
||||
extends
|
||||
AbstractJpaRepositoryWithMetadataManagement<JpaDistributionSet, DistributionSetManagement.Create, DistributionSetManagement.Update, DistributionSetRepository, DistributionSetFields, String, String>
|
||||
implements DistributionSetManagement<JpaDistributionSet> {
|
||||
|
||||
private final DistributionSetTagManagement<JpaDistributionSetTag> distributionSetTagManagement;
|
||||
@@ -104,6 +114,46 @@ public class JpaDistributionSetManagement
|
||||
this.repositoryProperties = repositoryProperties;
|
||||
}
|
||||
|
||||
private static final String COMPLETE = "complete";
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S3776") // java:S3776 - just too complex
|
||||
public Page<JpaDistributionSet> findByRsql(final String rsql, final Pageable pageable) {
|
||||
if (rsql != null && rsql.toLowerCase().contains(COMPLETE)) {
|
||||
// limited support for 'complete' - could be removed in future
|
||||
final Node node = RsqlParser.parse(rsql);
|
||||
final Specification<JpaDistributionSet> notDeleted = (root, query, cb) -> cb.equal(root.get(DELETED), false);
|
||||
final List<Specification<JpaDistributionSet>> specList = new ArrayList<>();
|
||||
specList.add(notDeleted);
|
||||
final AtomicReference<Node.Comparison> completedComparison = new AtomicReference<>();
|
||||
if (node instanceof Node.Comparison comparison && COMPLETE.equalsIgnoreCase(comparison.getKey())) {
|
||||
// all not deleted, won't add anything to spec
|
||||
completedComparison.set(comparison);
|
||||
} else if (node instanceof Node.Logical logical && logical.getOp() == Node.Logical.Operator.AND) {
|
||||
final List<Node> sanitizedChildren = new ArrayList<>();
|
||||
logical.getChildren().forEach(child -> {
|
||||
if (child instanceof Node.Comparison comparison && COMPLETE.equalsIgnoreCase(comparison.getKey())) {
|
||||
if (completedComparison.get() != null) {
|
||||
throw new RSQLParameterSyntaxException("Multiple 'complete' comparisons are not supported");
|
||||
}
|
||||
completedComparison.set(comparison);
|
||||
} else {
|
||||
sanitizedChildren.add(child);
|
||||
}
|
||||
});
|
||||
specList.add(QLSupport.getInstance()
|
||||
.buildSpec(new Node.Logical(Node.Logical.Operator.AND, sanitizedChildren), DistributionSetFields.class));
|
||||
}
|
||||
if (completedComparison.get() != null) { // really a comparison
|
||||
log.warn("Usage of 'complete' in RSQL is deprecated and will be removed in future: {}", node);
|
||||
final boolean completed = completeComparison(completedComparison);
|
||||
return filter(JpaManagementHelper.findAllWithCountBySpec(jpaRepository, specList, pageable), completed);
|
||||
}
|
||||
}
|
||||
|
||||
return super.findByRsql(rsql, pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaDistributionSet update(final Update update) {
|
||||
final JpaDistributionSet updated = super.update(update);
|
||||
@@ -199,7 +249,7 @@ public class JpaDistributionSetManagement
|
||||
if (distributionSet.isLocked()) {
|
||||
return jpaDistributionSet;
|
||||
} else {
|
||||
if (!jpaDistributionSet.isComplete()) {
|
||||
if (!distributionSet.isComplete()) {
|
||||
throw new IncompleteDistributionSetException("Could not be locked while incomplete!");
|
||||
}
|
||||
lockSoftwareModules(jpaDistributionSet);
|
||||
@@ -298,16 +348,13 @@ public class JpaDistributionSetManagement
|
||||
@Override
|
||||
public JpaDistributionSet getValidAndComplete(final long id) {
|
||||
final JpaDistributionSet distributionSet = getValid0(id);
|
||||
|
||||
if (!distributionSet.isComplete()) {
|
||||
throw new IncompleteDistributionSetException(
|
||||
"Distribution set of type " + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId());
|
||||
}
|
||||
|
||||
if (distributionSet.isDeleted()) {
|
||||
throw new DeletedException(DistributionSet.class, id);
|
||||
}
|
||||
|
||||
return distributionSet;
|
||||
}
|
||||
|
||||
@@ -357,6 +404,24 @@ public class JpaDistributionSetManagement
|
||||
QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class);
|
||||
}
|
||||
|
||||
private static boolean completeComparison(final AtomicReference<Node.Comparison> completeComparison) {
|
||||
final Node.Comparison comparison = completeComparison.get();
|
||||
if (comparison.getOp() == Node.Comparison.Operator.EQ) {
|
||||
return Boolean.parseBoolean(String.valueOf(comparison.getValue()));
|
||||
} else if (comparison.getOp() == Node.Comparison.Operator.NE) {
|
||||
return !Boolean.parseBoolean(String.valueOf(comparison.getValue()));
|
||||
} else {
|
||||
throw new RSQLParameterSyntaxException("Unsupported operator for 'complete': " + comparison.getOp());
|
||||
}
|
||||
}
|
||||
|
||||
private static Page<JpaDistributionSet> filter(final Page<JpaDistributionSet> page, final boolean completed) {
|
||||
final List<JpaDistributionSet> filtered = page.getContent().stream()
|
||||
.filter(ds -> ds.isComplete() == completed)
|
||||
.toList();
|
||||
return new PageImpl<>(filtered, page.getPageable(), page.getTotalElements());
|
||||
}
|
||||
|
||||
private static Collection<Long> notFound(final Collection<Long> distributionSetIds, final List<JpaDistributionSet> foundDistributionSets) {
|
||||
final Map<Long, JpaDistributionSet> foundDistributionSetMap = foundDistributionSets.stream()
|
||||
.collect(Collectors.toMap(JpaDistributionSet::getId, Function.identity()));
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException;
|
||||
import org.eclipse.hawkbit.repository.exception.LockedException;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
|
||||
@@ -142,6 +143,9 @@ public class JpaSoftwareModuleManagement extends
|
||||
if (jpaSoftwareModule.isLocked()) {
|
||||
return jpaSoftwareModule;
|
||||
} else {
|
||||
if (!softwareModule.isComplete()) {
|
||||
throw new IncompleteSoftwareModuleException("Could not be locked while incomplete!");
|
||||
}
|
||||
jpaSoftwareModule.lock();
|
||||
return jpaRepository.save(jpaSoftwareModule);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -45,13 +46,13 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetDeletedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.DistributionSetTypeUndefinedException;
|
||||
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.exception.LockedException;
|
||||
import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
@@ -122,9 +123,6 @@ public class JpaDistributionSet
|
||||
@Column(name = "meta_value", length = DistributionSet.METADATA_MAX_VALUE_SIZE)
|
||||
private Map<String, String> metadata = new HashMap<>();
|
||||
|
||||
@Column(name = "complete")
|
||||
private boolean complete;
|
||||
|
||||
@Setter
|
||||
@Column(name = "locked")
|
||||
private boolean locked;
|
||||
@@ -161,6 +159,20 @@ public class JpaDistributionSet
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComplete() {
|
||||
return Optional.ofNullable(type).map(dsType -> {
|
||||
if (getModules().stream().anyMatch(module -> !module.isComplete())) {
|
||||
return false; // incomplete module
|
||||
}
|
||||
final List<SoftwareModuleType> smTypes = getModules().stream()
|
||||
.map(SoftwareModule::getType)
|
||||
.distinct()
|
||||
.toList();
|
||||
return !smTypes.isEmpty() && new HashSet<>(smTypes).containsAll(dsType.getMandatoryModuleTypes());
|
||||
}).orElse(true);
|
||||
}
|
||||
|
||||
public void addModule(final SoftwareModule softwareModule) {
|
||||
if (isLocked()) {
|
||||
throw new LockedException(JpaDistributionSet.class, getId(), "ADD_SOFTWARE_MODULE");
|
||||
@@ -180,9 +192,7 @@ public class JpaDistributionSet
|
||||
.findAny().ifPresent(modules::remove);
|
||||
}
|
||||
|
||||
if (modules.add(softwareModule)) {
|
||||
complete = type.checkComplete(this);
|
||||
}
|
||||
modules.add(softwareModule);
|
||||
}
|
||||
|
||||
public void removeModule(final SoftwareModule softwareModule) {
|
||||
@@ -190,8 +200,8 @@ public class JpaDistributionSet
|
||||
throw new LockedException(JpaDistributionSet.class, getId(), "REMOVE_SOFTWARE_MODULE");
|
||||
}
|
||||
|
||||
if (modules != null && modules.removeIf(m -> m.getId().equals(softwareModule.getId()))) {
|
||||
complete = type.checkComplete(this);
|
||||
if (modules != null) {
|
||||
modules.removeIf(m -> m.getId().equals(softwareModule.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,20 +209,17 @@ public class JpaDistributionSet
|
||||
return Collections.unmodifiableSet(tags);
|
||||
}
|
||||
|
||||
public boolean addTag(final DistributionSetTag tag) {
|
||||
public void addTag(final DistributionSetTag tag) {
|
||||
if (tags == null) {
|
||||
tags = new HashSet<>();
|
||||
}
|
||||
|
||||
return tags.add(tag);
|
||||
tags.add(tag);
|
||||
}
|
||||
|
||||
public boolean removeTag(final DistributionSetTag tag) {
|
||||
if (tags == null) {
|
||||
return false;
|
||||
public void removeTag(final DistributionSetTag tag) {
|
||||
if (tags != null) {
|
||||
tags.remove(tag);
|
||||
}
|
||||
|
||||
return tags.remove(tag);
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
@@ -226,7 +233,7 @@ public class JpaDistributionSet
|
||||
|
||||
@Override
|
||||
public void fireUpdateEvent() {
|
||||
publishEventWithEventPublisher(new DistributionSetUpdatedEvent(this, complete));
|
||||
publishEventWithEventPublisher(new DistributionSetUpdatedEvent(this));
|
||||
|
||||
if (deleted) {
|
||||
publishEventWithEventPublisher(new DistributionSetDeletedEvent(getTenant(), getId(), getClass()));
|
||||
|
||||
@@ -112,15 +112,6 @@ public class JpaDistributionSetType extends AbstractJpaTypeEntity implements Dis
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkComplete(final DistributionSet distributionSet) {
|
||||
final List<SoftwareModuleType> smTypes = distributionSet.getModules().stream()
|
||||
.map(SoftwareModule::getType)
|
||||
.distinct()
|
||||
.toList();
|
||||
return !smTypes.isEmpty() && new HashSet<>(smTypes).containsAll(getMandatoryModuleTypes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DistributionSetType [key=" + getKey() + ", isDeleted()=" + isDeleted() + ", getId()=" + getId() + "]";
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.CollectionTable;
|
||||
@@ -146,6 +147,17 @@ public class JpaSoftwareModule
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComplete() {
|
||||
return Optional.ofNullable(type).map(smType -> {
|
||||
if (smType.getMinArtifacts() > 0) {
|
||||
return getArtifacts().size() >= smType.getMinArtifacts();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}).orElse(true);
|
||||
}
|
||||
|
||||
public void removeArtifact(final Artifact artifact) {
|
||||
if (isLocked()) {
|
||||
throw new LockedException(JpaSoftwareModule.class, getId(), "REMOVE_ARTIFACT");
|
||||
|
||||
@@ -45,6 +45,11 @@ public class JpaSoftwareModuleType extends AbstractJpaTypeEntity implements Soft
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Setter(value = lombok.AccessLevel.PRIVATE) // used via reflection
|
||||
@Column(name = "min_artifacts", nullable = false)
|
||||
@Min(0)
|
||||
private int minArtifacts;
|
||||
|
||||
@Setter(value = lombok.AccessLevel.PRIVATE) // used via reflection
|
||||
@Column(name = "max_ds_assignments", nullable = false)
|
||||
@Min(1)
|
||||
|
||||
@@ -57,26 +57,6 @@ public final class DistributionSetSpecification {
|
||||
return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.deleted), isDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Specification} for retrieving {@link DistributionSet}s by its COMPLETED attribute.
|
||||
*
|
||||
* @param isCompleted TRUE/FALSE are compared to the attribute COMPLETED. If NULL the attribute is ignored
|
||||
* @return the {@link DistributionSet} {@link Specification}
|
||||
*/
|
||||
public static Specification<JpaDistributionSet> isCompleted(final Boolean isCompleted) {
|
||||
return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.complete), isCompleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Specification} for retrieving {@link DistributionSet}s by its VALID attribute.
|
||||
*
|
||||
* @param isValid TRUE/FALSE are compared to the attribute VALID. If NULL the attribute is ignored
|
||||
* @return the {@link DistributionSet} {@link Specification}
|
||||
*/
|
||||
public static Specification<JpaDistributionSet> isValid(final Boolean isValid) {
|
||||
return (dsRoot, query, cb) -> cb.equal(dsRoot.get(JpaDistributionSet_.valid), isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Specification} for retrieving {@link DistributionSet} with given {@link DistributionSet#getId()}s.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user