Introduce soft deleted list option for soft deletable entities (#3093)

* Introduce soft deleted list option for soft deletable entities

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

* fix verify build

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

* fix typo in license

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

* Add sorting option on deleted field

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

* add missing import in tests

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

* Rename SoftDeletedFilter to SoftDeletedMode and its values

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

* Introduce MgmtSoftDeletedMode on api layer

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

* remove unused imports

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

* Integrate the enum on API layer

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

* Fix OpenApi spec

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

* address some comments

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

* Get rid of count(SoftDeletedMode) at all

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

* remove formatter for enum - stop supporting lowercase values in API

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

---------

Signed-off-by: strailov <Stanislav.Trailov@bosch.io>
This commit is contained in:
Stanislav Trailov
2026-06-08 16:37:05 +03:00
committed by GitHub
parent 95680962cc
commit f44b6268b0
45 changed files with 1230 additions and 101 deletions

View File

@@ -131,6 +131,7 @@ public class HawkbitMcpToolProvider {
request.getOffsetOrDefault(), request.getOffsetOrDefault(),
request.getLimitOrDefault(), request.getLimitOrDefault(),
null, null,
null,
null); null);
return toPagedResponse(response.getBody(), request); return toPagedResponse(response.getBody(), request);
@@ -148,6 +149,7 @@ public class HawkbitMcpToolProvider {
request.getRsqlOrNull(), request.getRsqlOrNull(),
request.getOffsetOrDefault(), request.getOffsetOrDefault(),
request.getLimitOrDefault(), request.getLimitOrDefault(),
null,
null); null);
return toPagedResponse(response.getBody(), request); return toPagedResponse(response.getBody(), request);
@@ -183,7 +185,7 @@ public class HawkbitMcpToolProvider {
request.getRsqlOrNull(), request.getRsqlOrNull(),
request.getOffsetOrDefault(), request.getOffsetOrDefault(),
request.getLimitOrDefault(), request.getLimitOrDefault(),
null); null, null);
return toPagedResponse(response.getBody(), request); return toPagedResponse(response.getBody(), request);
} }

View File

@@ -10,6 +10,8 @@
package org.eclipse.hawkbit.mgmt.rest.api; package org.eclipse.hawkbit.mgmt.rest.api;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.DISTRIBUTION_SET_ORDER; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.DISTRIBUTION_SET_ORDER;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT;
@@ -103,7 +105,10 @@ public interface MgmtDistributionSetRestApi {
@Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " + @Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " +
"A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." + "A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." +
"The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.") "The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.")
String sortParam); String sortParam,
@RequestParam(value = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, defaultValue = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT, required = false)
@Schema(description = "Controls whether soft-deleted distribution sets are included in the result.")
MgmtSoftDeletedMode softDeletedMode);
/** /**
* Handles the GET request of retrieving a single DistributionSet . * Handles the GET request of retrieving a single DistributionSet .

View File

@@ -10,6 +10,8 @@
package org.eclipse.hawkbit.mgmt.rest.api; package org.eclipse.hawkbit.mgmt.rest.api;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.DISTRIBUTION_SET_TYPE_ORDER; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.DISTRIBUTION_SET_TYPE_ORDER;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT;
@@ -91,7 +93,10 @@ public interface MgmtDistributionSetTypeRestApi {
@Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " + @Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " +
"A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." + "A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." +
"The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.") "The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.")
String sortParam); String sortParam,
@RequestParam(value = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, defaultValue = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT, required = false)
@Schema(description = "Controls whether soft-deleted distribution set types are included in the result.")
MgmtSoftDeletedMode softDeletedMode);
/** /**
* Handles the GET request of retrieving a single DistributionSetType within. * Handles the GET request of retrieving a single DistributionSetType within.

View File

@@ -39,6 +39,17 @@ public final class MgmtRestConstants {
* The request parameter for specifying the representation mode. The value of this parameter can either be "full" or "compact". * The request parameter for specifying the representation mode. The value of this parameter can either be "full" or "compact".
*/ */
public static final String REQUEST_PARAMETER_REPRESENTATION_MODE = "representation"; public static final String REQUEST_PARAMETER_REPRESENTATION_MODE = "representation";
/**
* The request parameter for specifying the soft deletion listing of entities. The value of this parameter
* can be ONLY_SOFT_DELETED, EXCLUDE_SOFT_DELETED or INCLUDE_SOFT_DELETED
*/
public static final String REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE = "soft_deleted_mode";
/**
* Default value of the soft deleted mode parameter.
*/
public static final String REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT = "EXCLUDE_SOFT_DELETED";
/** /**
* The default representation mode. * The default representation mode.
*/ */

View File

@@ -9,6 +9,8 @@
*/ */
package org.eclipse.hawkbit.mgmt.rest.api; package org.eclipse.hawkbit.mgmt.rest.api;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT;
@@ -93,7 +95,10 @@ public interface MgmtRolloutRestApi {
"The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.") "The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.")
String sortParam, String sortParam,
@RequestParam(value = REQUEST_PARAMETER_REPRESENTATION_MODE, defaultValue = REQUEST_PARAMETER_REPRESENTATION_MODE_DEFAULT) @RequestParam(value = REQUEST_PARAMETER_REPRESENTATION_MODE, defaultValue = REQUEST_PARAMETER_REPRESENTATION_MODE_DEFAULT)
String representationModeParam); String representationModeParam,
@RequestParam(value = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, defaultValue = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT, required = false)
@Schema(description = "Controls whether soft-deleted rollouts are included in the result.")
MgmtSoftDeletedMode softDeletedMode);
/** /**
* Handles the GET request of retrieving a single rollout. * Handles the GET request of retrieving a single rollout.

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.mgmt.rest.api;
import java.util.Optional;
public enum MgmtSoftDeletedMode {
ONLY_SOFT_DELETED,
EXCLUDE_SOFT_DELETED,
INCLUDE_SOFT_DELETED;
public static Optional<MgmtSoftDeletedMode> fromValue(final String value) {
if (value == null) {
return Optional.empty();
}
try {
return Optional.of(MgmtSoftDeletedMode.valueOf(value.toUpperCase()));
} catch (final IllegalArgumentException e) {
return Optional.empty();
}
}
}

View File

@@ -9,6 +9,8 @@
*/ */
package org.eclipse.hawkbit.mgmt.rest.api; package org.eclipse.hawkbit.mgmt.rest.api;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT;
@@ -186,7 +188,10 @@ public interface MgmtSoftwareModuleRestApi {
@Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " + @Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " +
"A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." + "A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." +
"The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.") "The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.")
String sortParam); String sortParam,
@RequestParam(value = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, defaultValue = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT, required = false)
@Schema(description = "Controls whether soft-deleted software modules are included in the result.")
MgmtSoftDeletedMode softDeletedMode);
/** /**
* Handles the GET request of retrieving a single software module. * Handles the GET request of retrieving a single software module.

View File

@@ -9,6 +9,8 @@
*/ */
package org.eclipse.hawkbit.mgmt.rest.api; package org.eclipse.hawkbit.mgmt.rest.api;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET;
import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT; import static org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT;
@@ -86,7 +88,10 @@ public interface MgmtSoftwareModuleTypeRestApi {
@Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " + @Schema(description = "The query parameter sort allows to define the sort order for the result of a query. " +
"A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." + "A sort criteria consists of the name of a field and the sort direction (ASC for ascending and DESC descending)." +
"The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.") "The sequence of the sort criteria (multiple can be used) defines the sort order of the entities in the result.")
String sortParam); String sortParam,
@RequestParam(value = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, defaultValue = REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE_DEFAULT, required = false)
@Schema(description = "Controls whether soft-deleted software module types are included in the result.")
MgmtSoftDeletedMode softDeletedMode);
/** /**
* Handles the GET request of retrieving a single software module type . * Handles the GET request of retrieving a single software module type .

View File

@@ -51,6 +51,8 @@ import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
@@ -112,16 +114,17 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
@Override @Override
public ResponseEntity<PagedList<MgmtDistributionSet>> getDistributionSets( public ResponseEntity<PagedList<MgmtDistributionSet>> getDistributionSets(
final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam, final MgmtSoftDeletedMode softDeletedModeParam) {
if (rsqlParam != null && rsqlParam.toLowerCase().contains("complete")) { if (rsqlParam != null && rsqlParam.toLowerCase().contains("complete")) {
LogUtility.logDeprecated("Usage of MgmtDistributionSetResource.getActions with 'complete': 'complete' distribution set search field is limited and may be removed."); LogUtility.logDeprecated("Usage of MgmtDistributionSetResource.getActions with 'complete': 'complete' distribution set search field is limited and may be removed.");
} }
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetSortParam(sortParam)); final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetSortParam(sortParam));
final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name());
final Page<? extends DistributionSet> findDsPage; final Page<? extends DistributionSet> findDsPage;
if (rsqlParam != null) { if (rsqlParam != null) {
findDsPage = distributionSetManagement.findByRsql(rsqlParam, pageable); findDsPage = distributionSetManagement.findByRsql(rsqlParam, softDeletedMode, pageable);
} else { } else {
findDsPage = distributionSetManagement.findAll(pageable); findDsPage = distributionSetManagement.findAll(softDeletedMode, pageable);
} }
final List<MgmtDistributionSet> rest = MgmtDistributionSetMapper.toResponseFromDsList(findDsPage.getContent()); final List<MgmtDistributionSet> rest = MgmtDistributionSetMapper.toResponseFromDsList(findDsPage.getContent());

View File

@@ -26,6 +26,8 @@ import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtDistributionSetTypeMapp
import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleTypeMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleTypeMapper;
import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility; import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.SoftwareModuleTypeNotInDistributionSetTypeException; import org.eclipse.hawkbit.repository.exception.SoftwareModuleTypeNotInDistributionSetTypeException;
@@ -59,21 +61,19 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR
@Override @Override
public ResponseEntity<PagedList<MgmtDistributionSetType>> getDistributionSetTypes( public ResponseEntity<PagedList<MgmtDistributionSetType>> getDistributionSetTypes(
final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam, final MgmtSoftDeletedMode softDeletedModeParam) {
final Pageable pageable = PagingUtility.toPageable( final Pageable pageable = PagingUtility.toPageable(
pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetTypeSortParam(sortParam)); pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetTypeSortParam(sortParam));
final Slice<? extends DistributionSetType> findModuleTypesAll; final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name());
long countModulesAll; final Page<? extends DistributionSetType> findModuleTypesAll;
if (rsqlParam != null) { if (rsqlParam != null) {
findModuleTypesAll = distributionSetTypeManagement.findByRsql(rsqlParam, pageable); findModuleTypesAll = distributionSetTypeManagement.findByRsql(rsqlParam, softDeletedMode, pageable);
countModulesAll = ((Page<?>) findModuleTypesAll).getTotalElements();
} else { } else {
findModuleTypesAll = distributionSetTypeManagement.findAll(pageable); findModuleTypesAll = distributionSetTypeManagement.findAll(softDeletedMode, pageable);
countModulesAll = distributionSetTypeManagement.count();
} }
final List<MgmtDistributionSetType> rest = MgmtDistributionSetTypeMapper.toListResponse(findModuleTypesAll.getContent()); final List<MgmtDistributionSetType> rest = MgmtDistributionSetTypeMapper.toListResponse(findModuleTypesAll.getContent());
return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); return ResponseEntity.ok(new PagedList<>(rest, findModuleTypesAll.getTotalElements()));
} }
@Override @Override

View File

@@ -36,6 +36,8 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.RolloutManagement.Create; import org.eclipse.hawkbit.repository.RolloutManagement.Create;
import org.eclipse.hawkbit.repository.RolloutManagement.GroupCreate; import org.eclipse.hawkbit.repository.RolloutManagement.GroupCreate;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
@@ -78,7 +80,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
@Override @Override
public ResponseEntity<PagedList<MgmtRolloutResponseBody>> getRollouts( public ResponseEntity<PagedList<MgmtRolloutResponseBody>> getRollouts(
final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam, final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam,
final String representationModeParam) { final String representationModeParam, final MgmtSoftDeletedMode softDeletedModeParam) {
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeRolloutSortParam(sortParam)); final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeRolloutSortParam(sortParam));
final boolean isFullMode = parseRepresentationMode(representationModeParam) == MgmtRepresentationMode.FULL; final boolean isFullMode = parseRepresentationMode(representationModeParam) == MgmtRepresentationMode.FULL;
@@ -90,9 +92,10 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
: rolloutManagement.findByRsqlWithDetailedStatus(rsqlParam, false, pageable); : rolloutManagement.findByRsqlWithDetailedStatus(rsqlParam, false, pageable);
rest = MgmtRolloutMapper.toResponseRolloutWithDetails(rollouts.getContent()); rest = MgmtRolloutMapper.toResponseRolloutWithDetails(rollouts.getContent());
} else { } else {
final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name());
rollouts = rsqlParam == null rollouts = rsqlParam == null
? rolloutManagement.findAll(false, pageable) ? rolloutManagement.findAll(softDeletedMode, pageable)
: rolloutManagement.findByRsql(rsqlParam, false, pageable); : rolloutManagement.findByRsql(rsqlParam, softDeletedMode, pageable);
rest = MgmtRolloutMapper.toResponseRollout(rollouts.getContent()); rest = MgmtRolloutMapper.toResponseRollout(rollouts.getContent());
} }
return ResponseEntity.ok(new PagedList<>(rest, rollouts.getTotalElements())); return ResponseEntity.ok(new PagedList<>(rest, rollouts.getTotalElements()));

View File

@@ -37,6 +37,8 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleMapper;
import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility; import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility;
import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.SystemManagement;
@@ -164,20 +166,19 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi {
@Override @Override
public ResponseEntity<PagedList<MgmtSoftwareModule>> getSoftwareModules( public ResponseEntity<PagedList<MgmtSoftwareModule>> getSoftwareModules(
final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam, final MgmtSoftDeletedMode softDeletedModeParam) {
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeSoftwareModuleSortParam(sortParam)); final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeSoftwareModuleSortParam(sortParam));
final Slice<? extends SoftwareModule> findModulesAll; final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name());
final long countModulesAll; final Page<? extends SoftwareModule> findModulesAll;
if (rsqlParam != null) { if (rsqlParam != null) {
findModulesAll = softwareModuleManagement.findByRsql(rsqlParam, pageable); findModulesAll = softwareModuleManagement.findByRsql(
countModulesAll = ((Page<?>) findModulesAll).getTotalElements(); rsqlParam, softDeletedMode, pageable);
} else { } else {
findModulesAll = softwareModuleManagement.findAll(pageable); findModulesAll = softwareModuleManagement.findAll(softDeletedMode, pageable);
countModulesAll = softwareModuleManagement.count();
} }
final List<MgmtSoftwareModule> rest = MgmtSoftwareModuleMapper.toResponse(findModulesAll.getContent()); final List<MgmtSoftwareModule> rest = MgmtSoftwareModuleMapper.toResponse(findModulesAll.getContent());
return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); return ResponseEntity.ok(new PagedList<>(rest, findModulesAll.getTotalElements()));
} }
@Override @Override

View File

@@ -21,6 +21,8 @@ import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModule
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleTypeMapper; import org.eclipse.hawkbit.mgmt.rest.resource.mapper.MgmtSoftwareModuleTypeMapper;
import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility; import org.eclipse.hawkbit.mgmt.rest.resource.util.PagingUtility;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
@@ -45,20 +47,18 @@ public class MgmtSoftwareModuleTypeResource implements MgmtSoftwareModuleTypeRes
@Override @Override
public ResponseEntity<PagedList<MgmtSoftwareModuleType>> getTypes( public ResponseEntity<PagedList<MgmtSoftwareModuleType>> getTypes(
final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final String rsqlParam, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam, final MgmtSoftDeletedMode softDeletedModeParam) {
final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeSoftwareModuleTypeSortParam(sortParam)); final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeSoftwareModuleTypeSortParam(sortParam));
final Slice<? extends SoftwareModuleType> findModuleTypessAll; final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name());
final long countModulesAll; final Page<? extends SoftwareModuleType> findModuleTypessAll;
if (rsqlParam != null) { if (rsqlParam != null) {
findModuleTypessAll = softwareModuleTypeManagement.findByRsql(rsqlParam, pageable); findModuleTypessAll = softwareModuleTypeManagement.findByRsql(rsqlParam, softDeletedMode, pageable);
countModulesAll = ((Page<?>) findModuleTypessAll).getTotalElements();
} else { } else {
findModuleTypessAll = softwareModuleTypeManagement.findAll(pageable); findModuleTypessAll = softwareModuleTypeManagement.findAll(softDeletedMode, pageable);
countModulesAll = softwareModuleTypeManagement.count();
} }
final List<MgmtSoftwareModuleType> rest = MgmtSoftwareModuleTypeMapper.toTypesResponse(findModuleTypessAll.getContent()); final List<MgmtSoftwareModuleType> rest = MgmtSoftwareModuleTypeMapper.toTypesResponse(findModuleTypessAll.getContent());
return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); return ResponseEntity.ok(new PagedList<>(rest, findModuleTypessAll.getTotalElements()));
} }
@Override @Override

View File

@@ -73,7 +73,7 @@ public final class MgmtTargetFilterQueryMapper {
targetRest.add( targetRest.add(
linkTo(methodOn(MgmtDistributionSetRestApi.class).getDistributionSets( linkTo(methodOn(MgmtDistributionSetRestApi.class).getDistributionSets(
"name==" + distributionSet.getName() + ";version==" + distributionSet.getVersion(), Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET), "name==" + distributionSet.getName() + ";version==" + distributionSet.getVersion(), Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET),
Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT), null Integer.parseInt(MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT), null, null
)).withRel("DS").expand()); )).withRel("DS").expand());
} }

View File

@@ -1026,6 +1026,144 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe
assertThat(distributionSetManagement.findAll(PAGE)).isEmpty(); assertThat(distributionSetManagement.findAll(PAGE)).isEmpty();
} }
@Test
void getDistributionSetsFilteredBySoftDeletedMode() throws Exception {
final DistributionSet activeDs = testdataFactory.createDistributionSet("active");
final DistributionSet deletedDs = testdataFactory.createDistributionSet("deleted");
testdataFactory.createTarget("dsTarget");
assignDistributionSet(deletedDs.getId(), "dsTarget");
distributionSetManagement.delete(deletedDs.getId());
// default — only active
mvc.perform(get("/rest/v1/distributionsets").accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(activeDs.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
// only_soft_deleted — only deleted
mvc.perform(get("/rest/v1/distributionsets")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(deletedDs.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// include_soft_deleted — both
mvc.perform(get("/rest/v1/distributionsets")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$.total", equalTo(2)));
// exclude_soft_deleted — explicit, same as default
mvc.perform(get("/rest/v1/distributionsets")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
}
@Test
void getDistributionSetsFilteredBySoftDeletedModeWithRsql() throws Exception {
final DistributionSet activeDs = testdataFactory.createDistributionSet("rsqlActive");
final DistributionSet deletedDs = testdataFactory.createDistributionSet("rsqlDeleted");
testdataFactory.createTarget("rsqlDsTarget");
assignDistributionSet(deletedDs.getId(), "rsqlDsTarget");
distributionSetManagement.delete(deletedDs.getId());
// rsql + soft_deleted — find deleted by name
mvc.perform(get("/rest/v1/distributionsets")
.param("q", "name==" + deletedDs.getName())
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(deletedDs.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// rsql + not_soft_deleted — deleted not found
mvc.perform(get("/rest/v1/distributionsets")
.param("q", "name==" + deletedDs.getName())
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.total", equalTo(0)));
// rsql + include_soft_deleted — filter by name narrows to one
mvc.perform(get("/rest/v1/distributionsets")
.param("q", "name==" + activeDs.getName())
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(activeDs.getName())));
}
@Test
void updateSoftDeletedDistributionSetRejected() throws Exception {
final DistributionSet ds = testdataFactory.createDistributionSet("toDelete");
testdataFactory.createTarget("updateTarget");
assignDistributionSet(ds.getId(), "updateTarget");
distributionSetManagement.delete(ds.getId());
final String body = new JSONObject().put("description", "updated").toString();
mvc.perform(put("/rest/v1/distributionsets/{dsId}", ds.getId()).content(body)
.contentType(APPLICATION_JSON).accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
@Test
void lockSoftDeletedDistributionSetRejected() throws Exception {
final DistributionSet ds = testdataFactory.createDistributionSet("toDelete");
testdataFactory.createTarget("lockTarget");
assignDistributionSet(ds.getId(), "lockTarget");
distributionSetManagement.delete(ds.getId());
final String body = new JSONObject().put("locked", true).toString();
mvc.perform(put("/rest/v1/distributionsets/{dsId}", ds.getId()).content(body)
.contentType(APPLICATION_JSON).accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
@Test
void assignSoftwareModuleToSoftDeletedDistributionSetRejected() throws Exception {
final DistributionSet ds = testdataFactory.createDistributionSet("toDelete");
testdataFactory.createTarget("assignSmTarget");
assignDistributionSet(ds.getId(), "assignSmTarget");
distributionSetManagement.delete(ds.getId());
final SoftwareModule sm = testdataFactory.createSoftwareModuleOs("newModule");
mvc.perform(post("/rest/v1/distributionsets/{dsId}/assignedSM", ds.getId())
.content("[{\"id\":" + sm.getId() + "}]")
.contentType(APPLICATION_JSON).accept(APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
/** /**
* Ensures that DS property update request to API is reflected by the repository. * Ensures that DS property update request to API is reflected by the repository.
*/ */

View File

@@ -493,6 +493,142 @@ class MgmtDistributionSetTypeResourceTest extends AbstractManagementApiIntegrati
assertThat(distributionSetTypeManagement.count()).isEqualTo(DEFAULT_DS_TYPES); assertThat(distributionSetTypeManagement.count()).isEqualTo(DEFAULT_DS_TYPES);
} }
@Test
void getDistributionSetTypesFilteredBySoftDeletedMode() throws Exception {
final DistributionSetType activeType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("activeKey").name("activeType").build());
// create type + DS using it, then delete type → soft-delete
final DistributionSetType deletedType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("deletedKey").name("deletedType").build());
distributionSetManagement.create(DistributionSetManagement.Create.builder()
.type(deletedType).name("ds").version("1.0").build());
distributionSetTypeManagement.delete(deletedType.getId());
// default — built-in + activeType, no deletedType
mvc.perform(get("/rest/v1/distributionsettypes").accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(DEFAULT_DS_TYPES + 1)))
.andExpect(jsonPath("$.total", equalTo(DEFAULT_DS_TYPES + 1)))
.andExpect(jsonPath("$.content.[?(@.key=='deletedKey')]").doesNotExist());
// only_soft_deleted — only deletedType
mvc.perform(get("/rest/v1/distributionsettypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("deletedType")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// include_soft_deleted — everything
mvc.perform(get("/rest/v1/distributionsettypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(DEFAULT_DS_TYPES + 2)))
.andExpect(jsonPath("$.total", equalTo(DEFAULT_DS_TYPES + 2)));
// exclude_soft_deleted — explicit, same as default
mvc.perform(get("/rest/v1/distributionsettypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(DEFAULT_DS_TYPES + 1)))
.andExpect(jsonPath("$.total", equalTo(DEFAULT_DS_TYPES + 1)))
.andExpect(jsonPath("$.content.[?(@.key=='deletedKey')]").doesNotExist());
}
@Test
void getDistributionSetTypesFilteredBySoftDeletedModeWithRsql() throws Exception {
distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("rsqlActiveKey").name("rsqlActiveType").build());
final DistributionSetType deletedType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("rsqlDeletedKey").name("rsqlDeletedType").build());
distributionSetManagement.create(DistributionSetManagement.Create.builder()
.type(deletedType).name("ds").version("1.0").build());
distributionSetTypeManagement.delete(deletedType.getId());
// rsql + soft_deleted — find deleted by name
mvc.perform(get("/rest/v1/distributionsettypes")
.param("q", "name==rsqlDeletedType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlDeletedType")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// rsql + not_soft_deleted — deleted not found
mvc.perform(get("/rest/v1/distributionsettypes")
.param("q", "name==rsqlDeletedType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.total", equalTo(0)));
// rsql + include_soft_deleted — filter by name narrows to one
mvc.perform(get("/rest/v1/distributionsettypes")
.param("q", "name==rsqlActiveType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlActiveType")));
}
@Test
void updateSoftDeletedDistributionSetTypeRejected() throws Exception {
final DistributionSetType deletedType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("delKey").name("delType").build());
distributionSetManagement.create(DistributionSetManagement.Create.builder()
.type(deletedType).name("ds").version("1.0").build());
distributionSetTypeManagement.delete(deletedType.getId());
final String body = new JSONObject().put("description", "updated").toString();
mvc.perform(put("/rest/v1/distributionsettypes/{dstId}", deletedType.getId()).content(body)
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
@Test
void assignSmTypeToSoftDeletedDistributionSetTypeRejected() throws Exception {
final DistributionSetType deletedType = distributionSetTypeManagement.create(
DistributionSetTypeManagement.Create.builder()
.key("delKey2").name("delType2").build());
distributionSetManagement.create(DistributionSetManagement.Create.builder()
.type(deletedType).name("ds2").version("1.0").build());
distributionSetTypeManagement.delete(deletedType.getId());
final SoftwareModuleType smType = softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("newSmType").name("newSmType").build());
mvc.perform(post("/rest/v1/distributionsettypes/{dstId}/mandatorymoduletypes", deletedType.getId())
.content("{\"id\":" + smType.getId() + "}")
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
/** /**
* Checks the correct behaviour of /rest/v1/distributionsettypes/{ID} PUT requests. * Checks the correct behaviour of /rest/v1/distributionsettypes/{ID} PUT requests.
*/ */

View File

@@ -1495,6 +1495,110 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest {
} }
} }
@Test
void getRolloutsFilteredBySoftDeletedMode() throws Exception {
testdataFactory.createTargets(20, "rolloutFilter", "rolloutFilter");
final DistributionSet dsA = testdataFactory.createDistributionSet("filterDs");
// create two rollouts
final Rollout rollout1 = createRollout("activeRollout", 4, dsA, "controllerId==rolloutFilter*");
final Rollout rollout2 = createRollout("toDeleteRollout", 4, dsA, "controllerId==rolloutFilter*");
// start and soft-delete rollout2
rolloutManagement.start(rollout2.getId());
rolloutHandler.handleAll();
rolloutManagement.delete(rollout2.getId());
rolloutHandler.handleAll();
// default (exclude_soft_deleted) — only active rollout
mvc.perform(get("/rest/v1/rollouts").accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(rollout1.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
// soft_deleted_mode=only_soft_deleted — only deleted rollout
mvc.perform(get("/rest/v1/rollouts")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(rollout2.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// soft_deleted_mode=include_soft_deleted — both rollouts
mvc.perform(get("/rest/v1/rollouts")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$.total", equalTo(2)));
// exclude_soft_deleted — explicit, same as default
mvc.perform(get("/rest/v1/rollouts")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(rollout1.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
}
@Test
void getRolloutsFilteredBySoftDeletedModeWithRsql() throws Exception {
testdataFactory.createTargets(20, "rolloutRsql", "rolloutRsql");
final DistributionSet dsA = testdataFactory.createDistributionSet("rsqlDs");
final Rollout rollout1 = createRollout("rsqlActive", 4, dsA, "controllerId==rolloutRsql*");
final Rollout rollout2 = createRollout("rsqlDeleted", 4, dsA, "controllerId==rolloutRsql*");
// start and soft-delete rollout2
rolloutManagement.start(rollout2.getId());
rolloutHandler.handleAll();
rolloutManagement.delete(rollout2.getId());
rolloutHandler.handleAll();
// rsql + only_soft_deleted — find deleted rollout by name
mvc.perform(get("/rest/v1/rollouts")
.param("q", "name==rsqlDeleted")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(rollout2.getName())))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// rsql + not_soft_deleted — deleted rollout not found
mvc.perform(get("/rest/v1/rollouts")
.param("q", "name==rsqlDeleted")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.total", equalTo(0)));
// rsql + include_soft_deleted — both visible, filter by name narrows to one
mvc.perform(get("/rest/v1/rollouts")
.param("q", "name==rsqlActive")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo(rollout1.getName())));
}
@Test @Test
void stopRunningRollout() throws Exception { void stopRunningRollout() throws Exception {
final Rollout rollout = testdataFactory.createAndStartRollout(); final Rollout rollout = testdataFactory.createAndStartRollout();

View File

@@ -1228,6 +1228,130 @@ class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegrationTes
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.rest.param.rsqlInvalidField"))); .andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.rest.param.rsqlInvalidField")));
} }
@Test
void getSoftwareModulesFilteredBySoftDeletedMode() throws Exception {
final SoftwareModule activeSm = softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("activeSm").version("1.0").build());
SoftwareModule deletedSm = softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("deletedSm").version("1.0").build());
testdataFactory.createDistributionSet(List.of(deletedSm));
softwareModuleManagement.delete(deletedSm.getId());
// default — only active
mvc.perform(get("/rest/v1/softwaremodules").accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("activeSm")))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
// only_soft_deleted — only deleted
mvc.perform(get("/rest/v1/softwaremodules")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("deletedSm")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// include_soft_deleted — both
mvc.perform(get("/rest/v1/softwaremodules")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$.total", equalTo(2)));
// exclude_soft_deleted — explicit, same as default
mvc.perform(get("/rest/v1/softwaremodules")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].deleted", equalTo(false)));
}
@Test
void getSoftwareModulesFilteredBySoftDeletedModeWithRsql() throws Exception {
softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("rsqlActive").version("1.0").build());
SoftwareModule deletedSm = softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("rsqlDeleted").version("1.0").build());
testdataFactory.createDistributionSet(List.of(deletedSm));
softwareModuleManagement.delete(deletedSm.getId());
// rsql + soft_deleted — find deleted by name
mvc.perform(get("/rest/v1/softwaremodules")
.param("q", "name==rsqlDeleted")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlDeleted")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// rsql + not_soft_deleted — deleted not found
mvc.perform(get("/rest/v1/softwaremodules")
.param("q", "name==rsqlDeleted")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.total", equalTo(0)));
// rsql + include_soft_deleted — filter by name narrows to one
mvc.perform(get("/rest/v1/softwaremodules")
.param("q", "name==rsqlActive")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlActive")));
}
@Test
void updateSoftDeletedSoftwareModuleRejected() throws Exception {
SoftwareModule sm = softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("delSm").version("1.0").build());
testdataFactory.createDistributionSet(List.of(sm));
softwareModuleManagement.delete(sm.getId());
final String body = new JSONObject().put("description", "updated").toString();
mvc.perform(put("/rest/v1/softwaremodules/{smId}", sm.getId()).content(body)
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
@Test
void lockSoftDeletedSoftwareModuleRejected() throws Exception {
SoftwareModule sm = softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(osType).name("delSmLock").version("1.0").build());
testdataFactory.createDistributionSet(List.of(sm));
softwareModuleManagement.delete(sm.getId());
final String body = new JSONObject().put("locked", true).toString();
mvc.perform(put("/rest/v1/softwaremodules/{smId}", sm.getId()).content(body)
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
/** /**
* Tests GET request on /rest/v1/softwaremodules/{smId}. * Tests GET request on /rest/v1/softwaremodules/{smId}.
*/ */

View File

@@ -489,6 +489,121 @@ public class MgmtSoftwareModuleTypeResourceTest extends AbstractManagementApiInt
} }
@Test
void getSoftwareModuleTypesFilteredBySoftDeletedMode() throws Exception {
// 3 built-in types exist (os, runtime, application)
final int builtInTypes = 3;
final SoftwareModuleType activeType = softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("activeKey").name("activeType").build());
// create type + SM using it, then delete type → soft-delete
final SoftwareModuleType deletedType = softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("deletedKey").name("deletedType").build());
softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(deletedType).name("sm").version("1.0").build());
softwareModuleTypeManagement.delete(deletedType.getId());
// default — built-in + activeType, no deletedType
mvc.perform(get("/rest/v1/softwaremoduletypes").accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(builtInTypes + 1)))
.andExpect(jsonPath("$.total", equalTo(builtInTypes + 1)))
.andExpect(jsonPath("$.content.[?(@.key=='deletedKey')]").doesNotExist());
// only_soft_deleted — only deletedType
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("deletedType")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// include_soft_deleted — everything
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(builtInTypes + 2)))
.andExpect(jsonPath("$.total", equalTo(builtInTypes + 2)));
// exclude_soft_deleted — explicit, same as default
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(builtInTypes + 1)))
.andExpect(jsonPath("$.total", equalTo(builtInTypes + 1)))
.andExpect(jsonPath("$.content.[?(@.key=='deletedKey')]").doesNotExist());
}
@Test
void getSoftwareModuleTypesFilteredBySoftDeletedModeWithRsql() throws Exception {
softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("rsqlActiveKey").name("rsqlActiveType").build());
final SoftwareModuleType deletedType = softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("rsqlDeletedKey").name("rsqlDeletedType").build());
softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(deletedType).name("sm").version("1.0").build());
softwareModuleTypeManagement.delete(deletedType.getId());
// rsql + soft_deleted — find deleted by name
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param("q", "name==rsqlDeletedType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "ONLY_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlDeletedType")))
.andExpect(jsonPath("content[0].deleted", equalTo(true)));
// rsql + not_soft_deleted — deleted not found
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param("q", "name==rsqlDeletedType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "EXCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(0)))
.andExpect(jsonPath("$.total", equalTo(0)));
// rsql + include_soft_deleted — filter by name narrows to one
mvc.perform(get("/rest/v1/softwaremoduletypes")
.param("q", "name==rsqlActiveType")
.param(MgmtRestConstants.REQUEST_PARAMETER_LIST_SOFT_DELETED_MODE, "INCLUDE_SOFT_DELETED")
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.total", equalTo(1)))
.andExpect(jsonPath("content[0].name", equalTo("rsqlActiveType")));
}
@Test
void updateSoftDeletedSoftwareModuleTypeRejected() throws Exception {
final SoftwareModuleType deletedType = softwareModuleTypeManagement.create(
SoftwareModuleTypeManagement.Create.builder().key("delKey").name("delType").build());
softwareModuleManagement.create(
SoftwareModuleManagement.Create.builder().type(deletedType).name("sm").version("1.0").build());
softwareModuleTypeManagement.delete(deletedType.getId());
final String body = new JSONObject().put("description", "updated").toString();
mvc.perform(put("/rest/v1/softwaremoduletypes/{smtId}", deletedType.getId()).content(body)
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.deleted")));
}
private SoftwareModuleType createTestType() { private SoftwareModuleType createTestType() {
final SoftwareModuleType testType = softwareModuleTypeManagement.create(SoftwareModuleTypeManagement.Create.builder() final SoftwareModuleType testType = softwareModuleTypeManagement.create(SoftwareModuleTypeManagement.Create.builder()
.key("test123").name("TestName123").description("Desc123").colour("colour").maxAssignments(5).build()); .key("test123").name("TestName123").description("Desc123").colour("colour").maxAssignments(5).build());

View File

@@ -13,3 +13,8 @@ logging.level.root=WARN
#logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG #logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG
# Logging END # Logging END
hawkbit.events.remote.enabled=false hawkbit.events.remote.enabled=false
logging.level.org.eclipse.persistence=DEBUG
spring.jpa.properties.eclipselink.logging.level=FINE
spring.jpa.properties.eclipselink.logging.level.sql=FINE
spring.jpa.properties.eclipselink.logging.parameters=true

View File

@@ -50,7 +50,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
* Management service for {@link DistributionSet}s. * Management service for {@link DistributionSet}s.
*/ */
public interface DistributionSetManagement<T extends DistributionSet> public interface DistributionSetManagement<T extends DistributionSet>
extends RepositoryManagement<T, DistributionSetManagement.Create, DistributionSetManagement.Update>, MetadataSupport<String> { extends SoftDeletableRepositoryManagement<T, DistributionSetManagement.Create, DistributionSetManagement.Update>, MetadataSupport<String> {
@Override @Override
default String permissionGroup() { default String permissionGroup() {

View File

@@ -37,7 +37,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
* Management service for {@link DistributionSetType}s. * Management service for {@link DistributionSetType}s.
*/ */
public interface DistributionSetTypeManagement<T extends DistributionSetType> public interface DistributionSetTypeManagement<T extends DistributionSetType>
extends RepositoryManagement<T, DistributionSetTypeManagement.Create, DistributionSetTypeManagement.Update> { extends SoftDeletableRepositoryManagement<T, DistributionSetTypeManagement.Create, DistributionSetTypeManagement.Update> {
@Override @Override
default String permissionGroup() { default String permissionGroup() {

View File

@@ -164,6 +164,16 @@ public interface RolloutManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY) @PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findAll(boolean deleted, @NotNull Pageable pageable); Page<Rollout> findAll(boolean deleted, @NotNull Pageable pageable);
/**
* Retrieves all rollouts filtered by their soft-deleted state.
*
* @param softDeletedMode the filter defining which rollouts to return based on their soft-deleted status
* @param pageable the page request to sort and limit the result
* @return a page of found rollouts
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findAll(SoftDeletedMode softDeletedMode, @NotNull Pageable pageable);
/** /**
* Get count of targets in different status in rollout. * Get count of targets in different status in rollout.
* *
@@ -188,6 +198,20 @@ public interface RolloutManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY) @PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findByRsql(@NotNull String rsql, boolean deleted, @NotNull Pageable pageable); Page<Rollout> findByRsql(@NotNull String rsql, boolean deleted, @NotNull Pageable pageable);
/**
* Retrieves all rollouts matching the given RSQL filter, filtered by their soft-deleted state.
*
* @param rsql the specification to filter rollouts
* @param softDeletedMode the filter defining which rollouts to return based on their soft-deleted status
* @param pageable the page request to sort and limit the result
* @return a page of found rollouts
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the given
* {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Rollout> findByRsql(@NotNull String rsql, SoftDeletedMode softDeletedMode, @NotNull Pageable pageable);
/** /**
* Finds rollouts by given text in name or description. * Finds rollouts by given text in name or description.
* *

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
import jakarta.validation.constraints.NotNull;
import org.eclipse.hawkbit.auth.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* Extension of {@link RepositoryManagement} for entities that support soft deletion,
* providing query methods that allow filtering by soft-deleted state.
*
* @param <T> type of the {@link BaseEntity}
* @param <C> type of the create request
* @param <U> type of the update request
*/
public interface SoftDeletableRepositoryManagement<T extends BaseEntity, C, U extends Identifiable<Long>>
extends RepositoryManagement<T, C, U> {
/**
* Retrieves a {@link Page} of all {@link BaseEntity}s filtered by their soft-deleted state.
*
* @param softDeletedMode the filter defining which entities to return based on their soft-deleted status
* @param pageable the page request to sort and limit the result
* @return a page of found entities matching the given soft-deleted filter
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<T> findAll(@NotNull SoftDeletedMode softDeletedMode, @NotNull Pageable pageable);
/**
* Retrieves a {@link Page} of {@link BaseEntity}s matching the given RSQL filter,
* filtered by their soft-deleted state.
*
* @param rsql filter definition in RSQL syntax
* @param softDeletedMode the filter defining which entities to return based on their soft-deleted status
* @param pageable the page request to sort and limit the result
* @return a page of found entities matching both the RSQL filter and the soft-deleted filter
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<T> findByRsql(@NotNull String rsql, @NotNull SoftDeletedMode softDeletedMode, @NotNull Pageable pageable);
}

View File

@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
public enum SoftDeletedMode {
ONLY_SOFT_DELETED,
EXCLUDE_SOFT_DELETED,
INCLUDE_SOFT_DELETED
}

View File

@@ -37,7 +37,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
* Service for managing {@link SoftwareModule}s. * Service for managing {@link SoftwareModule}s.
*/ */
public interface SoftwareModuleManagement<T extends SoftwareModule> public interface SoftwareModuleManagement<T extends SoftwareModule>
extends RepositoryManagement<T, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update>, MetadataSupport<MetadataValue> { extends SoftDeletableRepositoryManagement<T, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update>, MetadataSupport<MetadataValue> {
@Override @Override
default String permissionGroup() { default String permissionGroup() {

View File

@@ -31,7 +31,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
* Service for managing {@link SoftwareModuleType}s. * Service for managing {@link SoftwareModuleType}s.
*/ */
public interface SoftwareModuleTypeManagement<T extends SoftwareModuleType> public interface SoftwareModuleTypeManagement<T extends SoftwareModuleType>
extends RepositoryManagement<T, SoftwareModuleTypeManagement.Create, SoftwareModuleTypeManagement.Update> { extends SoftDeletableRepositoryManagement<T, SoftwareModuleTypeManagement.Create, SoftwareModuleTypeManagement.Update> {
@Override @Override
default String permissionGroup() { default String permissionGroup() {

View File

@@ -38,6 +38,7 @@ import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.Identifiable; import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.RepositoryManagement; import org.eclipse.hawkbit.repository.RepositoryManagement;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.jpa.Jpa; import org.eclipse.hawkbit.repository.jpa.Jpa;
@@ -382,7 +383,13 @@ abstract class AbstractJpaRepositoryManagement<T extends AbstractJpaBaseEntity,
private final Supplier<Optional<Specification<T>>> isNotDeletedSupplier = SingletonSupplier.of(this::isNotDeleted0); private final Supplier<Optional<Specification<T>>> isNotDeletedSupplier = SingletonSupplier.of(this::isNotDeleted0);
private Optional<Specification<T>> isNotDeleted() { protected Optional<Specification<T>> isSoftDeleted() {
return supportSoftDelete()
? Optional.of((root, query, cb) -> cb.equal(root.get(DELETED), true))
: Optional.empty();
}
protected Optional<Specification<T>> isNotDeleted() {
return isNotDeletedSupplier.get(); return isNotDeletedSupplier.get();
} }

View File

@@ -30,8 +30,10 @@ import org.eclipse.hawkbit.ql.Node;
import org.eclipse.hawkbit.ql.jpa.QLSupport; import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.exception.DeletedException; import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
@@ -105,46 +107,23 @@ public class JpaDistributionSetManagement
@Override @Override
@SuppressWarnings("java:S3776") // java:S3776 - just too complex @SuppressWarnings("java:S3776") // java:S3776 - just too complex
public Page<JpaDistributionSet> findByRsql(final String rsql, final Pageable pageable) { public Page<JpaDistributionSet> findByRsql(final String rsql, final Pageable pageable) {
if (rsql != null && rsql.toLowerCase().contains(COMPLETE)) { return findByRsqlAndDeleted(rsql, SoftDeletedMode.EXCLUDE_SOFT_DELETED, pageable);
// limited support for 'complete' - could be removed in future }
final Node node = QLSupport.getInstance().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(
sanitizedChildren.size() == 1
? sanitizedChildren.get(0)
: new Node.Logical(Node.Logical.Operator.AND, sanitizedChildren),
DistributionSetFields.class));
}
if (completedComparison.get() != null) { // really a comparison
log.warn("Usage of 'complete' is limited and may be removed: {}", node);
final boolean completed = completeComparison(completedComparison);
return filter(JpaManagementHelper.findAllWithCountBySpec(jpaRepository, specList, pageable), completed);
}
}
return super.findByRsql(rsql, pageable); @Override
public Page<JpaDistributionSet> findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) {
return findByRsqlAndDeleted(rsql, softDeletedMode, pageable);
}
@Override
public Page<JpaDistributionSet> findAll(SoftDeletedMode softDeletedMode, Pageable pageable) {
return jpaRepository.findAll(deletedSpecification(softDeletedMode), pageable);
} }
@Override @Override
public JpaDistributionSet update(final Update update) { public JpaDistributionSet update(final Update update) {
assertDistributionSetIsNotDeleted(jpaRepository.getById(update.getId()));
final JpaDistributionSet updated = super.update(update); final JpaDistributionSet updated = super.update(update);
if (Boolean.TRUE.equals(update.getLocked())) { if (Boolean.TRUE.equals(update.getLocked())) {
lockSoftwareModules(updated); lockSoftwareModules(updated);
@@ -154,6 +133,10 @@ public class JpaDistributionSetManagement
@Override @Override
public Map<Long, JpaDistributionSet> update(final Collection<Update> updates) { public Map<Long, JpaDistributionSet> update(final Collection<Update> updates) {
final List<Long> ids = updates.stream().map(Identifiable::getId).toList();
jpaRepository.findAllById(ids)
.forEach(this::assertDistributionSetIsNotDeleted);
final Map<Long, JpaDistributionSet> updated = super.update(updates); final Map<Long, JpaDistributionSet> updated = super.update(updates);
for (final Update update : updates) { for (final Update update : updates) {
final JpaDistributionSet updatedSet = updated.get(update.getId()); final JpaDistributionSet updatedSet = updated.get(update.getId());
@@ -228,6 +211,8 @@ public class JpaDistributionSetManagement
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaDistributionSet lock(final DistributionSet distributionSet) { public JpaDistributionSet lock(final DistributionSet distributionSet) {
final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet);
assertDistributionSetIsNotDeleted(jpaDistributionSet);
if (distributionSet.isLocked()) { if (distributionSet.isLocked()) {
return jpaDistributionSet; return jpaDistributionSet;
} else { } else {
@@ -245,6 +230,8 @@ public class JpaDistributionSetManagement
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaDistributionSet unlock(final DistributionSet distributionSet) { public JpaDistributionSet unlock(final DistributionSet distributionSet) {
final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet);
assertDistributionSetIsNotDeleted(jpaDistributionSet);
if (jpaDistributionSet.isLocked()) { if (jpaDistributionSet.isLocked()) {
jpaDistributionSet.setLocked(false); jpaDistributionSet.setLocked(false);
return jpaRepository.save(jpaDistributionSet); return jpaRepository.save(jpaDistributionSet);
@@ -257,6 +244,7 @@ public class JpaDistributionSetManagement
@Transactional @Transactional
public JpaDistributionSet invalidate(final DistributionSet distributionSet) { public JpaDistributionSet invalidate(final DistributionSet distributionSet) {
final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet);
assertDistributionSetIsNotDeleted(jpaDistributionSet);
jpaDistributionSet.invalidate(); jpaDistributionSet.invalidate();
return jpaRepository.save(jpaDistributionSet); return jpaRepository.save(jpaDistributionSet);
} }
@@ -296,6 +284,7 @@ public class JpaDistributionSetManagement
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public List<JpaDistributionSet> assignTag(final Collection<Long> ids, final long dsTagId) { public List<JpaDistributionSet> assignTag(final Collection<Long> ids, final long dsTagId) {
return updateTag(ids, dsTagId, (tag, distributionSet) -> { return updateTag(ids, dsTagId, (tag, distributionSet) -> {
assertDistributionSetIsNotDeleted(distributionSet);
if (distributionSet.getTags().contains(tag)) { if (distributionSet.getTags().contains(tag)) {
return distributionSet; return distributionSet;
} else { } else {
@@ -310,6 +299,7 @@ public class JpaDistributionSetManagement
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public List<JpaDistributionSet> unassignTag(final Collection<Long> ids, final long dsTagId) { public List<JpaDistributionSet> unassignTag(final Collection<Long> ids, final long dsTagId) {
return updateTag(ids, dsTagId, (tag, distributionSet) -> { return updateTag(ids, dsTagId, (tag, distributionSet) -> {
assertDistributionSetIsNotDeleted(distributionSet);
if (distributionSet.getTags().contains(tag)) { if (distributionSet.getTags().contains(tag)) {
distributionSet.removeTag(tag); distributionSet.removeTag(tag);
return jpaRepository.save(distributionSet); return jpaRepository.save(distributionSet);
@@ -332,9 +322,6 @@ public class JpaDistributionSetManagement
throw new IncompleteDistributionSetException( throw new IncompleteDistributionSetException(
"Distribution set of type " + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); "Distribution set of type " + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId());
} }
if (distributionSet.isDeleted()) {
throw new DeletedException(DistributionSet.class, id);
}
return distributionSet; return distributionSet;
} }
@@ -384,6 +371,62 @@ public class JpaDistributionSetManagement
QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class); QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class);
} }
private Page<JpaDistributionSet> findByRsqlAndDeleted(String rsql, SoftDeletedMode deletedMode, Pageable pageable) {
if (rsql != null && rsql.toLowerCase().contains(COMPLETE)) {
// limited support for 'complete' - could be removed in future
final Node node = QLSupport.getInstance().parse(rsql);
final Specification<JpaDistributionSet> deletedSpec = deletedSpecification(deletedMode);
final List<Specification<JpaDistributionSet>> specList = new ArrayList<>();
specList.add(deletedSpec);
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(
sanitizedChildren.size() == 1
? sanitizedChildren.get(0)
: new Node.Logical(Node.Logical.Operator.AND, sanitizedChildren),
DistributionSetFields.class));
}
if (completedComparison.get() != null) { // really a comparison
log.warn("Usage of 'complete' is limited and may be removed: {}", node);
final boolean completed = completeComparison(completedComparison);
return filter(JpaManagementHelper.findAllWithCountBySpec(jpaRepository, specList, pageable), completed);
}
}
// fallback
final List<Specification<JpaDistributionSet>> specList = new ArrayList<>();
final Specification<JpaDistributionSet> rslqSpec =
rsql != null ? QLSupport.getInstance().buildSpec(rsql, DistributionSetFields.class) : Specification.unrestricted();
specList.add(deletedSpecification(deletedMode));
specList.add(rslqSpec);
return JpaManagementHelper.findAllWithCountBySpec(jpaRepository, specList, pageable);
}
private Specification<JpaDistributionSet> deletedSpecification(final SoftDeletedMode softDeletedMode) {
return switch (softDeletedMode) {
case ONLY_SOFT_DELETED ->
(root, query, cb) -> cb.equal(root.get(DELETED), true);
case EXCLUDE_SOFT_DELETED ->
(root, query, cb) -> cb.equal(root.get(DELETED), false);
case INCLUDE_SOFT_DELETED ->
Specification.unrestricted();
};
}
private static boolean completeComparison(final AtomicReference<Node.Comparison> completeComparison) { private static boolean completeComparison(final AtomicReference<Node.Comparison> completeComparison) {
final Node.Comparison comparison = completeComparison.get(); final Node.Comparison comparison = completeComparison.get();
if (comparison.getOp() == Node.Comparison.Operator.EQ) { if (comparison.getOp() == Node.Comparison.Operator.EQ) {
@@ -410,6 +453,7 @@ public class JpaDistributionSetManagement
private JpaDistributionSet getValid0(final long id) { private JpaDistributionSet getValid0(final long id) {
final JpaDistributionSet distributionSet = jpaRepository.getById(id); final JpaDistributionSet distributionSet = jpaRepository.getById(id);
assertDistributionSetIsNotDeleted(distributionSet);
if (!distributionSet.isValid()) { if (!distributionSet.isValid()) {
throw new InvalidDistributionSetException( throw new InvalidDistributionSetException(
"Distribution set of type " + distributionSet.getType().getKey() + " is invalid: " + distributionSet.getId()); "Distribution set of type " + distributionSet.getType().getKey() + " is invalid: " + distributionSet.getId());
@@ -473,4 +517,10 @@ public class JpaDistributionSetManagement
throw new EntityNotFoundException(DistributionSetTag.class, tagId); throw new EntityNotFoundException(DistributionSetTag.class, tagId);
} }
} }
private void assertDistributionSetIsNotDeleted(final JpaDistributionSet jpaDistributionSet) {
if (jpaDistributionSet.isDeleted()) {
throw new DeletedException(DistributionSet.class, jpaDistributionSet.getId());
}
}
} }

View File

@@ -11,15 +11,21 @@ package org.eclipse.hawkbit.repository.jpa.management;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController; import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
@@ -38,6 +44,9 @@ import org.eclipse.hawkbit.tenancy.TenantAwareCacheManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.resilience.annotation.Retryable; import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -48,6 +57,7 @@ public class JpaDistributionSetTypeManagement
extends AbstractJpaRepositoryManagement<JpaDistributionSetType, DistributionSetTypeManagement.Create, DistributionSetTypeManagement.Update, DistributionSetTypeRepository, DistributionSetTypeFields> extends AbstractJpaRepositoryManagement<JpaDistributionSetType, DistributionSetTypeManagement.Create, DistributionSetTypeManagement.Update, DistributionSetTypeRepository, DistributionSetTypeFields>
implements DistributionSetTypeManagement<JpaDistributionSetType> { implements DistributionSetTypeManagement<JpaDistributionSetType> {
private final DistributionSetTypeRepository distributionSetTypeRepository;
private final SoftwareModuleTypeRepository softwareModuleTypeRepository; private final SoftwareModuleTypeRepository softwareModuleTypeRepository;
private final DistributionSetRepository distributionSetRepository; private final DistributionSetRepository distributionSetRepository;
private final TargetTypeRepository targetTypeRepository; private final TargetTypeRepository targetTypeRepository;
@@ -60,6 +70,7 @@ public class JpaDistributionSetTypeManagement
final DistributionSetRepository distributionSetRepository, final TargetTypeRepository targetTypeRepository, final DistributionSetRepository distributionSetRepository, final TargetTypeRepository targetTypeRepository,
final QuotaManagement quotaManagement) { final QuotaManagement quotaManagement) {
super(distributionSetTypeRepository, entityManager); super(distributionSetTypeRepository, entityManager);
this.distributionSetTypeRepository = distributionSetTypeRepository;
this.softwareModuleTypeRepository = softwareModuleTypeRepository; this.softwareModuleTypeRepository = softwareModuleTypeRepository;
this.distributionSetRepository = distributionSetRepository; this.distributionSetRepository = distributionSetRepository;
this.targetTypeRepository = targetTypeRepository; this.targetTypeRepository = targetTypeRepository;
@@ -117,11 +128,53 @@ public class JpaDistributionSetTypeManagement
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaDistributionSetType unassignSoftwareModuleType(final long id, final long softwareModuleTypeId) { public JpaDistributionSetType unassignSoftwareModuleType(final long id, final long softwareModuleTypeId) {
final JpaDistributionSetType type = jpaRepository.getById(id); final JpaDistributionSetType type = jpaRepository.getById(id);
assertDistributionSetTypeIsNotDeleted(type);
checkDistributionSetTypeNotAssigned(id); checkDistributionSetTypeNotAssigned(id);
type.removeModuleType(softwareModuleTypeRepository.getById(softwareModuleTypeId)); type.removeModuleType(softwareModuleTypeRepository.getById(softwareModuleTypeId));
return jpaRepository.save(type); return jpaRepository.save(type);
} }
@Override
public Page<JpaDistributionSetType> findAll(SoftDeletedMode softDeletedMode, Pageable pageable) {
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaDistributionSetType> deletedSpec =
DistributionSetTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return distributionSetTypeRepository.findAll(deletedSpec, pageable);
}
return distributionSetTypeRepository.findAll(pageable);
}
@Override
public Page<JpaDistributionSetType> findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) {
final Specification<JpaDistributionSetType> rsqlSpec = QLSupport.getInstance().buildSpec(rsql, DistributionSetTypeFields.class);
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaDistributionSetType> deletedSpec =
DistributionSetTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return distributionSetTypeRepository.findAll(JpaManagementHelper.combineWithAnd(List.of(rsqlSpec, deletedSpec)), pageable);
}
return distributionSetTypeRepository.findAll(rsqlSpec, pageable);
}
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaDistributionSetType update(final DistributionSetTypeManagement.Update update) {
final JpaDistributionSetType distributionSetType = distributionSetTypeRepository.getById(update.getId());
assertDistributionSetTypeIsNotDeleted(distributionSetType);
return super.update(update);
}
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public Map<Long, JpaDistributionSetType> update(final Collection<DistributionSetTypeManagement.Update> updates) {
final List<Long> ids = updates.stream().map(Identifiable::getId).toList();
distributionSetTypeRepository.findAllById(ids).forEach(this::assertDistributionSetTypeIsNotDeleted);
return super.update(updates);
}
private JpaDistributionSetType assignSoftwareModuleTypes( private JpaDistributionSetType assignSoftwareModuleTypes(
final long dsTypeId, final Collection<Long> softwareModulesTypeIds, final boolean mandatory) { final long dsTypeId, final Collection<Long> softwareModulesTypeIds, final boolean mandatory) {
final Collection<JpaSoftwareModuleType> foundModules = softwareModuleTypeRepository.findAllById(softwareModulesTypeIds); final Collection<JpaSoftwareModuleType> foundModules = softwareModuleTypeRepository.findAllById(softwareModulesTypeIds);
@@ -132,6 +185,7 @@ public class JpaDistributionSetTypeManagement
final JpaDistributionSetType type = jpaRepository.getById(dsTypeId); final JpaDistributionSetType type = jpaRepository.getById(dsTypeId);
assertDistributionSetTypeIsNotDeleted(type);
checkDistributionSetTypeNotAssigned(dsTypeId); checkDistributionSetTypeNotAssigned(dsTypeId);
assertSoftwareModuleTypeQuota(dsTypeId, softwareModulesTypeIds.size()); assertSoftwareModuleTypeQuota(dsTypeId, softwareModulesTypeIds.size());
@@ -168,4 +222,10 @@ public class JpaDistributionSetTypeManagement
"Distribution set type %s is already assigned to distribution sets and cannot be changed!", id)); "Distribution set type %s is already assigned to distribution sets and cannot be changed!", id));
} }
} }
private void assertDistributionSetTypeIsNotDeleted(final DistributionSetType distributionSetType){
if (distributionSetType.isDeleted()) {
throw new DeletedException(DistributionSetType.class, distributionSetType.getId());
}
}
} }

View File

@@ -41,6 +41,7 @@ import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutHelper; import org.eclipse.hawkbit.repository.RolloutHelper;
import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.RolloutStatusCache; import org.eclipse.hawkbit.repository.RolloutStatusCache;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder; import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent;
@@ -266,6 +267,18 @@ public class JpaRolloutManagement implements RolloutManagement {
rolloutRepository.findAll(RolloutSpecification.isDeleted(deleted, pageable.getSort()), pageable), pageable); rolloutRepository.findAll(RolloutSpecification.isDeleted(deleted, pageable.getSort()), pageable), pageable);
} }
@Override
public Page<Rollout> findAll(final SoftDeletedMode softDeletedMode, Pageable pageable) {
return switch (softDeletedMode) {
case EXCLUDE_SOFT_DELETED -> JpaManagementHelper.convertPage(
rolloutRepository.findAll(RolloutSpecification.isDeleted(false, pageable.getSort()), pageable), pageable);
case ONLY_SOFT_DELETED -> JpaManagementHelper.convertPage(
rolloutRepository.findAll(RolloutSpecification.isDeleted(true, pageable.getSort()), pageable), pageable);
case INCLUDE_SOFT_DELETED -> JpaManagementHelper.convertPage(
rolloutRepository.findAll(Specification.unrestricted(), pageable), pageable);
};
}
@Override @Override
public Page<Rollout> findAllWithDetailedStatus(final boolean deleted, final Pageable pageable) { public Page<Rollout> findAllWithDetailedStatus(final boolean deleted, final Pageable pageable) {
return appendStatusDetails(JpaManagementHelper.convertPage( return appendStatusDetails(JpaManagementHelper.convertPage(
@@ -281,6 +294,21 @@ public class JpaRolloutManagement implements RolloutManagement {
return JpaManagementHelper.convertPage(rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(specList), pageable), pageable); return JpaManagementHelper.convertPage(rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(specList), pageable), pageable);
} }
@Override
public Page<Rollout> findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) {
final Specification<JpaRollout> rsqlSpec = QLSupport.getInstance().buildSpec(rsql, RolloutFields.class);
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaRollout> deletedSpec = RolloutSpecification.isDeleted(
softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED, pageable.getSort());
return JpaManagementHelper.convertPage(
rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(List.of(rsqlSpec, deletedSpec)), pageable), pageable);
} else {
return JpaManagementHelper.convertPage(rolloutRepository.findAll(rsqlSpec, pageable), pageable);
}
}
@Override @Override
public Page<Rollout> findByRsqlWithDetailedStatus(final String rsql, final boolean deleted, final Pageable pageable) { public Page<Rollout> findByRsqlWithDetailedStatus(final String rsql, final boolean deleted, final Pageable pageable) {
final List<Specification<JpaRollout>> specList = List.of( final List<Specification<JpaRollout>> specList = List.of(

View File

@@ -22,9 +22,13 @@ import java.util.stream.Collectors;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionService; import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionService;
import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException; import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException;
import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.exception.LockedException;
@@ -45,6 +49,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProp
import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.resilience.annotation.Retryable; import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -56,6 +61,7 @@ public class JpaSoftwareModuleManagement extends
AbstractJpaRepositoryWithMetadataManagement<JpaSoftwareModule, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update, SoftwareModuleRepository, SoftwareModuleFields, MetadataValue, JpaSoftwareModule.JpaMetadataValue> AbstractJpaRepositoryWithMetadataManagement<JpaSoftwareModule, SoftwareModuleManagement.Create, SoftwareModuleManagement.Update, SoftwareModuleRepository, SoftwareModuleFields, MetadataValue, JpaSoftwareModule.JpaMetadataValue>
implements SoftwareModuleManagement<JpaSoftwareModule> { implements SoftwareModuleManagement<JpaSoftwareModule> {
private final SoftwareModuleRepository softwareModuleRepository;
private final DistributionSetRepository distributionSetRepository; private final DistributionSetRepository distributionSetRepository;
private final ArtifactManagement artifactManagement; private final ArtifactManagement artifactManagement;
private final QuotaManagement quotaManagement; private final QuotaManagement quotaManagement;
@@ -64,6 +70,7 @@ public class JpaSoftwareModuleManagement extends
final DistributionSetRepository distributionSetRepository, final ArtifactManagement artifactManagement, final DistributionSetRepository distributionSetRepository, final ArtifactManagement artifactManagement,
final QuotaManagement quotaManagement) { final QuotaManagement quotaManagement) {
super(softwareModuleRepository, entityManager); super(softwareModuleRepository, entityManager);
this.softwareModuleRepository = softwareModuleRepository;
this.distributionSetRepository = distributionSetRepository; this.distributionSetRepository = distributionSetRepository;
this.artifactManagement = artifactManagement; this.artifactManagement = artifactManagement;
this.quotaManagement = quotaManagement; this.quotaManagement = quotaManagement;
@@ -96,6 +103,25 @@ public class JpaSoftwareModuleManagement extends
return createdModule; return createdModule;
} }
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaSoftwareModule update(final Update update) {
final JpaSoftwareModule softwareModule = softwareModuleRepository.getById(update.getId());
assertSoftwareModuleIsNotDeleted(softwareModule);
return super.update(update);
}
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public Map<Long, JpaSoftwareModule> update(final Collection<SoftwareModuleManagement.Update> updates) {
final List<Long> ids = updates.stream().map(Identifiable::getId).toList();
softwareModuleRepository.findAllById(ids).forEach(this::assertSoftwareModuleIsNotDeleted);
return super.update(updates);
}
@Override @Override
protected List<JpaSoftwareModule> softDelete(final Collection<JpaSoftwareModule> toDelete) { protected List<JpaSoftwareModule> softDelete(final Collection<JpaSoftwareModule> toDelete) {
return toDelete.stream().filter(swModule -> { return toDelete.stream().filter(swModule -> {
@@ -138,6 +164,7 @@ public class JpaSoftwareModuleManagement extends
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaSoftwareModule lock(final SoftwareModule softwareModule) { public JpaSoftwareModule lock(final SoftwareModule softwareModule) {
final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule); final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule);
assertSoftwareModuleIsNotDeleted(jpaSoftwareModule);
if (jpaSoftwareModule.isLocked()) { if (jpaSoftwareModule.isLocked()) {
return jpaSoftwareModule; return jpaSoftwareModule;
} else { } else {
@@ -154,6 +181,7 @@ public class JpaSoftwareModuleManagement extends
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaSoftwareModule unlock(final SoftwareModule softwareModule) { public JpaSoftwareModule unlock(final SoftwareModule softwareModule) {
final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule); final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule);
assertSoftwareModuleIsNotDeleted(jpaSoftwareModule);
if (softwareModule.isLocked()) { if (softwareModule.isLocked()) {
jpaSoftwareModule.unlock(); jpaSoftwareModule.unlock();
return jpaRepository.save(jpaSoftwareModule); return jpaRepository.save(jpaSoftwareModule);
@@ -162,6 +190,30 @@ public class JpaSoftwareModuleManagement extends
} }
} }
@Override
public Page<JpaSoftwareModule> findAll(SoftDeletedMode softDeletedMode, Pageable pageable) {
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
Specification<JpaSoftwareModule> softDeletedSpec =
SoftwareModuleSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return softwareModuleRepository.findAll(softDeletedSpec, pageable);
}
return softwareModuleRepository.findAll(pageable);
}
@Override
public Page<JpaSoftwareModule> findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) {
final Specification<JpaSoftwareModule> rsqlSpec = QLSupport.getInstance().buildSpec(rsql, SoftwareModuleFields.class);
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaSoftwareModule> softDeletedSpec =
SoftwareModuleSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return softwareModuleRepository.findAll(
JpaManagementHelper.combineWithAnd(List.of(rsqlSpec, softDeletedSpec)), pageable);
}
return softwareModuleRepository.findAll(rsqlSpec, pageable);
}
@Override @Override
public Page<JpaSoftwareModule> findByAssignedTo(final long distributionSetId, final Pageable pageable) { public Page<JpaSoftwareModule> findByAssignedTo(final long distributionSetId, final Pageable pageable) {
assertDistributionSetExists(distributionSetId); assertDistributionSetExists(distributionSetId);
@@ -201,4 +253,10 @@ public class JpaSoftwareModuleManagement extends
throw new EntityNotFoundException(DistributionSet.class, distributionSetId); throw new EntityNotFoundException(DistributionSet.class, distributionSetId);
} }
} }
private void assertSoftwareModuleIsNotDeleted(final SoftwareModule softwareModule) {
if (softwareModule.isDeleted()) {
throw new DeletedException(SoftwareModule.class, softwareModule.getId());
}
}
} }

View File

@@ -10,20 +10,36 @@
package org.eclipse.hawkbit.repository.jpa.management; package org.eclipse.hawkbit.repository.jpa.management;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.eclipse.hawkbit.ql.jpa.QLSupport;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.SoftDeletedMode;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository;
import org.eclipse.hawkbit.repository.jpa.specifications.SoftwareModuleTypeSpecification;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.qfields.SoftwareModuleTypeFields; import org.eclipse.hawkbit.repository.qfields.SoftwareModuleTypeFields;
import org.eclipse.hawkbit.tenancy.TenantAwareCacheManager; import org.eclipse.hawkbit.tenancy.TenantAwareCacheManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service @Service
@ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "software-module-type=management" }, matchIfMissing = true) @ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "software-module-type=management" }, matchIfMissing = true)
@@ -31,6 +47,7 @@ public class JpaSoftwareModuleTypeManagement
extends AbstractJpaRepositoryManagement<JpaSoftwareModuleType, SoftwareModuleTypeManagement.Create, SoftwareModuleTypeManagement.Update, SoftwareModuleTypeRepository, SoftwareModuleTypeFields> extends AbstractJpaRepositoryManagement<JpaSoftwareModuleType, SoftwareModuleTypeManagement.Create, SoftwareModuleTypeManagement.Update, SoftwareModuleTypeRepository, SoftwareModuleTypeFields>
implements SoftwareModuleTypeManagement<JpaSoftwareModuleType> { implements SoftwareModuleTypeManagement<JpaSoftwareModuleType> {
private final SoftwareModuleTypeRepository softwareModuleTypeRepository;
private final DistributionSetTypeRepository distributionSetTypeRepository; private final DistributionSetTypeRepository distributionSetTypeRepository;
private final SoftwareModuleRepository softwareModuleRepository; private final SoftwareModuleRepository softwareModuleRepository;
@@ -40,6 +57,7 @@ public class JpaSoftwareModuleTypeManagement
final DistributionSetTypeRepository distributionSetTypeRepository, final DistributionSetTypeRepository distributionSetTypeRepository,
final SoftwareModuleRepository softwareModuleRepository) { final SoftwareModuleRepository softwareModuleRepository) {
super(softwareModuleTypeRepository, entityManager); super(softwareModuleTypeRepository, entityManager);
this.softwareModuleTypeRepository = softwareModuleTypeRepository;
this.distributionSetTypeRepository = distributionSetTypeRepository; this.distributionSetTypeRepository = distributionSetTypeRepository;
this.softwareModuleRepository = softwareModuleRepository; this.softwareModuleRepository = softwareModuleRepository;
} }
@@ -54,9 +72,56 @@ public class JpaSoftwareModuleTypeManagement
return jpaRepository.findByKey(key); return jpaRepository.findByKey(key);
} }
@Override
public Page<JpaSoftwareModuleType> findAll(SoftDeletedMode softDeletedMode, Pageable pageable) {
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaSoftwareModuleType> deletedSpec =
SoftwareModuleTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return softwareModuleTypeRepository.findAll(deletedSpec, pageable);
}
return softwareModuleTypeRepository.findAll(pageable);
}
@Override
public Page<JpaSoftwareModuleType> findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) {
final Specification<JpaSoftwareModuleType> rsqlSpec = QLSupport.getInstance().buildSpec(rsql, SoftwareModuleTypeFields.class);
if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) {
final Specification<JpaSoftwareModuleType> deletedSpec =
SoftwareModuleTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED);
return softwareModuleTypeRepository.findAll(JpaManagementHelper.combineWithAnd(List.of(rsqlSpec, deletedSpec)), pageable);
}
return softwareModuleTypeRepository.findAll(rsqlSpec, pageable);
}
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public JpaSoftwareModuleType update(final SoftwareModuleTypeManagement.Update update) {
final JpaSoftwareModuleType softwareModuleType = softwareModuleTypeRepository.getById(update.getId());
assertSoftwareModuleTypeIsNotDeleted(softwareModuleType);
return super.update(update);
}
@Override
@Transactional
@Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY)
public Map<Long, JpaSoftwareModuleType> update(final Collection<SoftwareModuleTypeManagement.Update> updates) {
final List<Long> ids = updates.stream().map(Identifiable::getId).toList();
softwareModuleTypeRepository.findAllById(ids).forEach(this::assertSoftwareModuleTypeIsNotDeleted);
return super.update(updates);
}
@Override @Override
protected Collection<JpaSoftwareModuleType> softDelete(final Collection<JpaSoftwareModuleType> toDelete) { protected Collection<JpaSoftwareModuleType> softDelete(final Collection<JpaSoftwareModuleType> toDelete) {
return toDelete.stream().filter(smt -> return toDelete.stream().filter(smt ->
softwareModuleRepository.countByType(smt) > 0 || distributionSetTypeRepository.countByElementsSmType(smt) > 0).toList(); softwareModuleRepository.countByType(smt) > 0 || distributionSetTypeRepository.countByElementsSmType(smt) > 0).toList();
} }
private void assertSoftwareModuleTypeIsNotDeleted(final JpaSoftwareModuleType softwareModuleType){
if (softwareModuleType.isDeleted()) {
throw new DeletedException(SoftwareModuleType.class, softwareModuleType.getId());
}
}
} }

View File

@@ -13,6 +13,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaTypeEntity_; import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaTypeEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType_;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
@@ -34,4 +35,9 @@ public final class DistributionSetTypeSpecification {
public static Specification<JpaDistributionSetType> byKey(final String key) { public static Specification<JpaDistributionSetType> byKey(final String key) {
return (targetRoot, query, cb) -> cb.equal(targetRoot.get(AbstractJpaTypeEntity_.key), key); return (targetRoot, query, cb) -> cb.equal(targetRoot.get(AbstractJpaTypeEntity_.key), key);
} }
public static Specification<JpaDistributionSetType> isDeleted(final Boolean isDeleted) {
return (root, query, cb) ->
cb.equal(root.<Boolean> get(JpaDistributionSetType_.deleted), isDeleted);
}
} }

View File

@@ -32,4 +32,9 @@ public final class SoftwareModuleSpecification {
return cb.equal(join.get(AbstractJpaBaseEntity_.ID), dsId); return cb.equal(join.get(AbstractJpaBaseEntity_.ID), dsId);
}; };
} }
public static Specification<JpaSoftwareModule> isDeleted(final Boolean isDeleted) {
return (root, query, cb) ->
cb.equal(root.<Boolean> get(JpaSoftwareModule_.deleted), isDeleted);
}
} }

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.specifications;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType_;
import org.springframework.data.jpa.domain.Specification;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SoftwareModuleTypeSpecification {
public static Specification<JpaSoftwareModuleType> isDeleted(final Boolean isDeleted) {
return (root, query, cb) ->
cb.equal(root.<Boolean> get(JpaSoftwareModuleType_.deleted), isDeleted);
}
}

View File

@@ -457,16 +457,16 @@ public abstract class AbstractRepositoryManagementTest<T extends BaseEntity, C,
if (resolvableType.getRawClass() == targetSuperClass) { if (resolvableType.getRawClass() == targetSuperClass) {
return Optional.of(resolvableType); return Optional.of(resolvableType);
} }
final Optional<ResolvableType> inInterfaces = Arrays.stream(resolvableType.getInterfaces()) for (final ResolvableType superInterface : resolvableType.getInterfaces()) {
.filter(superInterface -> superInterface.getRawClass() == targetSuperClass) final Optional<ResolvableType> found = findGenericSuperType(superInterface, targetSuperClass);
.findAny(); if (found.isPresent()) {
if (inInterfaces.isPresent()) { return found;
return inInterfaces; }
} else if (resolvableType.getSuperType() != ResolvableType.NONE) {
return findGenericSuperType(resolvableType.getSuperType(), targetSuperClass);
} else {
return Optional.empty();
} }
if (resolvableType.getSuperType() != ResolvableType.NONE) {
return findGenericSuperType(resolvableType.getSuperType(), targetSuperClass);
}
return Optional.empty();
} }
private void assertEquals(final Object actual, final Object expected, final Deque<String> path) { private void assertEquals(final Object actual, final Object expected, final Deque<String> path) {

View File

@@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement.Update;
import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.repository.Identifiable; import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
@@ -936,4 +937,19 @@ class DistributionSetManagementTest extends AbstractRepositoryManagementWithMeta
.as("entity with too short version should not be updated") .as("entity with too short version should not be updated")
.isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2)); .isThrownBy(() -> distributionSetManagement.update(distributionSetUpdate2));
} }
@Test
void bulkUpdateSoftDeletedDistributionSetRejected() {
final DistributionSet active = testdataFactory.createDistributionSet("active");
final DistributionSet toDelete = testdataFactory.createDistributionSet("toDelete");
testdataFactory.createTarget("bulkTarget");
assignDistributionSet(toDelete.getId(), "bulkTarget");
distributionSetManagement.delete(toDelete.getId());
final List<Update> updates = List.of(
Update.builder().id(active.getId()).description("ok").build(),
Update.builder().id(toDelete.getId()).description("should fail").build());
assertThatThrownBy(() -> distributionSetManagement.update(updates))
.isInstanceOf(DeletedException.class);
}
} }

View File

@@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Create;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Update; import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Update;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException; import org.eclipse.hawkbit.repository.exception.IncompleteSoftwareModuleException;
import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.exception.LockedException;
import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream; import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream;
@@ -486,6 +487,22 @@ class SoftwareModuleManagementTest
} }
} }
@Test
void bulkUpdateSoftDeletedSoftwareModuleRejected() {
final SoftwareModule active = softwareModuleManagement.create(
Create.builder().type(osType).name("active").version("1.0").build());
SoftwareModule toDelete = softwareModuleManagement.create(
Create.builder().type(osType).name("toDelete").version("1.0").build());
testdataFactory.createDistributionSet(List.of(toDelete));
softwareModuleManagement.delete(toDelete.getId());
final List<Update> updates = List.of(
Update.builder().id(active.getId()).description("ok").build(),
Update.builder().id(toDelete.getId()).description("should fail").build());
assertThatExceptionOfType(DeletedException.class)
.isThrownBy(() -> softwareModuleManagement.update(updates));
}
private void assertArtifactDoesntExist(final Artifact... results) { private void assertArtifactDoesntExist(final Artifact... results) {
for (final Artifact result : results) { for (final Artifact result : results) {
final String currentTenant = AccessContext.tenant(); final String currentTenant = AccessContext.tenant();

View File

@@ -95,7 +95,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
(query, rsqlFilter) -> Optional.ofNullable( (query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query .getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query
.getSortOrders())) .getSortOrders()), null)
.getBody()) .getBody())
.stream().flatMap(body -> body.getContent().stream()), .stream().flatMap(body -> body.getContent().stream()),
e -> new CreateDialog(hawkbitClient).result(), e -> new CreateDialog(hawkbitClient).result(),
@@ -158,7 +158,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
type.setItemLabelGenerator(MgmtDistributionSetType::getName); type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(Optional.ofNullable( type.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC) .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC, null)
.getBody()) .getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElseGet(Collections::emptyList)); .orElseGet(Collections::emptyList));
@@ -266,7 +266,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC) .getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC, null)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0])); .orElseGet(() -> new MgmtDistributionSetType[0]));
@@ -353,6 +353,8 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
v -> new Utils.BaseDialog<Void>("Add Software Modules") { v -> new Utils.BaseDialog<Void>("Add Software Modules") {
{ {
setHeight("80vh");
setWidth("80vw");
final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient); final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
add(softwareModulesView); add(softwareModulesView);
final Button addBtn = new Button("Add"); final Button addBtn = new Button("Add");

View File

@@ -96,7 +96,7 @@ public final class RolloutView extends TableView<MgmtRolloutResponseBody, Long>
(query, rsqlFilter) -> Optional.ofNullable( (query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi() hawkbitClient.getRolloutRestApi()
.getRollouts( .getRollouts(
rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC, "full") rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC, "full", null)
.getBody()).stream().flatMap(page -> page.getContent().stream()), .getBody()).stream().flatMap(page -> page.getContent().stream()),
selectionGrid -> new CreateDialog(hawkbitClient).result(), selectionGrid -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> { selectionGrid -> {
@@ -344,7 +344,7 @@ public final class RolloutView extends TableView<MgmtRolloutResponseBody, Long>
"Distribution Set", "Distribution Set",
this::readyToCreate, this::readyToCreate,
query -> hawkbitClient.getDistributionSetRestApi() query -> hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(query.getFilter().orElse(null), query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getDistributionSets(query.getFilter().orElse(null), query.getOffset(), query.getPageSize(), Constants.NAME_ASC, null)
.getBody().getContent().stream()); .getBody().getContent().stream());
distributionSet.setRequiredIndicatorVisible(true); distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());

View File

@@ -98,7 +98,8 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
}, },
(query, rsqlFilter) -> Optional.ofNullable( (query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi() hawkbitClient.getSoftwareModuleRestApi()
.getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC,
null)
.getBody()) .getBody())
.stream().map(PagedList::getContent).flatMap(List::stream), .stream().map(PagedList::getContent).flatMap(List::stream),
isParent ? v -> new CreateDialog(hawkbitClient).result() : null, isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
@@ -137,7 +138,7 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName); type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.setItems(Optional.ofNullable( type.setItems(Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi() hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 20, Constants.NAME_ASC) .getTypes(null, 0, 20, Constants.NAME_ASC, null)
.getBody()) .getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElseGet(Collections::emptyList)); .orElseGet(Collections::emptyList));
@@ -231,7 +232,7 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi() hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 30, Constants.NAME_ASC) .getTypes(null, 0, 30, Constants.NAME_ASC, null)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0])) .map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0]))
.orElseGet(() -> new MgmtSoftwareModuleType[0])); .orElseGet(() -> new MgmtSoftwareModuleType[0]));
@@ -264,7 +265,7 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
distType.setItems( distType.setItems(
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC, null)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0])); .orElseGet(() -> new MgmtDistributionSetType[0]));

View File

@@ -256,7 +256,8 @@ public class TargetFilterQueryView extends TableView<TargetFilterQueryView.Targe
query.getFilter().orElse(null), query.getFilter().orElse(null),
query.getOffset(), query.getOffset(),
query.getLimit(), query.getLimit(),
Constants.NAME_ASC) Constants.NAME_ASC,
null)
.getBody()).stream().flatMap(body -> body.getContent().stream())); .getBody()).stream().flatMap(body -> body.getContent().stream()));
distributionSet.setItemLabelGenerator(ds -> ds.getName() + ":" + ds.getVersion()); distributionSet.setItemLabelGenerator(ds -> ds.getName() + ":" + ds.getVersion());
distributionSet.focus(); distributionSet.focus();

View File

@@ -869,7 +869,7 @@ public final class TargetView extends TableView<TargetView.TargetWithDs, String>
"Distribution Set", "Distribution Set",
this::readyToAssign, this::readyToAssign,
query -> hawkbitClient.getDistributionSetRestApi() query -> hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(query.getFilter().orElse(null), query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getDistributionSets(query.getFilter().orElse(null), query.getOffset(), query.getPageSize(), Constants.NAME_ASC, null)
.getBody().getContent().stream()); .getBody().getContent().stream());
distributionSet.setRequiredIndicatorVisible(true); distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());