diff --git a/hawkbit-mcp/hawkbit-mcp-starter/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java b/hawkbit-mcp/hawkbit-mcp-starter/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java index 27ccea715..3b5449cbe 100644 --- a/hawkbit-mcp/hawkbit-mcp-starter/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java +++ b/hawkbit-mcp/hawkbit-mcp-starter/src/main/java/org/eclipse/hawkbit/mcp/server/tools/HawkbitMcpToolProvider.java @@ -131,6 +131,7 @@ public class HawkbitMcpToolProvider { request.getOffsetOrDefault(), request.getLimitOrDefault(), null, + null, null); return toPagedResponse(response.getBody(), request); @@ -148,6 +149,7 @@ public class HawkbitMcpToolProvider { request.getRsqlOrNull(), request.getOffsetOrDefault(), request.getLimitOrDefault(), + null, null); return toPagedResponse(response.getBody(), request); @@ -183,7 +185,7 @@ public class HawkbitMcpToolProvider { request.getRsqlOrNull(), request.getOffsetOrDefault(), request.getLimitOrDefault(), - null); + null, null); return toPagedResponse(response.getBody(), request); } diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java index 2df7b3df0..db8a44a5c 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetRestApi.java @@ -10,6 +10,8 @@ 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.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_OFFSET; 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. " + "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.") - 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 . diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetTypeRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetTypeRestApi.java index 811a0f0b5..f55be5810 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetTypeRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtDistributionSetTypeRestApi.java @@ -10,6 +10,8 @@ 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.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_OFFSET; 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. " + "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.") - 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. diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java index 937805bb4..af20b90ce 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java @@ -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". */ 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. */ diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java index 234d487fc..46854ac47 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRolloutRestApi.java @@ -9,6 +9,8 @@ */ 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_OFFSET; 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.") String sortParam, @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. diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftDeletedMode.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftDeletedMode.java new file mode 100644 index 000000000..b5e3a8249 --- /dev/null +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftDeletedMode.java @@ -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 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(); + } + } +} diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleRestApi.java index 80092fc4a..504aab831 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleRestApi.java @@ -9,6 +9,8 @@ */ 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_OFFSET; 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. " + "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.") - 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. diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleTypeRestApi.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleTypeRestApi.java index ba2acf8bc..535a235b1 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleTypeRestApi.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtSoftwareModuleTypeRestApi.java @@ -9,6 +9,8 @@ */ 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_OFFSET; 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. " + "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.") - 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 . diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java index 07eb2f5d0..222432121 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java @@ -28,4 +28,4 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @ComponentScan @Import({ RestConfiguration.class, OpenApi.class }) @PropertySource("classpath:/hawkbit-mgmt-api-defaults.properties") -public class MgmtApiConfiguration implements WebMvcConfigurer {} \ No newline at end of file +public class MgmtApiConfiguration implements WebMvcConfigurer {} diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index 9c91a51ae..00e16f170 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -51,6 +51,8 @@ import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetInvalidationManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; 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.SystemManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; @@ -112,16 +114,17 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { @Override public ResponseEntity> 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")) { 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 SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name()); final Page findDsPage; if (rsqlParam != null) { - findDsPage = distributionSetManagement.findByRsql(rsqlParam, pageable); + findDsPage = distributionSetManagement.findByRsql(rsqlParam, softDeletedMode, pageable); } else { - findDsPage = distributionSetManagement.findAll(pageable); + findDsPage = distributionSetManagement.findAll(softDeletedMode, pageable); } final List rest = MgmtDistributionSetMapper.toResponseFromDsList(findDsPage.getContent()); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResource.java index 3bad25c31..7e0531e48 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResource.java @@ -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.util.PagingUtility; 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.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.SoftwareModuleTypeNotInDistributionSetTypeException; @@ -59,21 +61,19 @@ public class MgmtDistributionSetTypeResource implements MgmtDistributionSetTypeR @Override public ResponseEntity> 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( pagingOffsetParam, pagingLimitParam, sanitizeDistributionSetTypeSortParam(sortParam)); - final Slice findModuleTypesAll; - long countModulesAll; + final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name()); + final Page findModuleTypesAll; if (rsqlParam != null) { - findModuleTypesAll = distributionSetTypeManagement.findByRsql(rsqlParam, pageable); - countModulesAll = ((Page) findModuleTypesAll).getTotalElements(); + findModuleTypesAll = distributionSetTypeManagement.findByRsql(rsqlParam, softDeletedMode, pageable); } else { - findModuleTypesAll = distributionSetTypeManagement.findAll(pageable); - countModulesAll = distributionSetTypeManagement.count(); + findModuleTypesAll = distributionSetTypeManagement.findAll(softDeletedMode, pageable); } final List rest = MgmtDistributionSetTypeMapper.toListResponse(findModuleTypesAll.getContent()); - return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); + return ResponseEntity.ok(new PagedList<>(rest, findModuleTypesAll.getTotalElements())); } @Override diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java index 2c95e604a..9206a6410 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResource.java @@ -36,6 +36,8 @@ import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutManagement.Create; 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.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; @@ -78,7 +80,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { @Override public ResponseEntity> getRollouts( 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 boolean isFullMode = parseRepresentationMode(representationModeParam) == MgmtRepresentationMode.FULL; @@ -90,9 +92,10 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi { : rolloutManagement.findByRsqlWithDetailedStatus(rsqlParam, false, pageable); rest = MgmtRolloutMapper.toResponseRolloutWithDetails(rollouts.getContent()); } else { + final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name()); rollouts = rsqlParam == null - ? rolloutManagement.findAll(false, pageable) - : rolloutManagement.findByRsql(rsqlParam, false, pageable); + ? rolloutManagement.findAll(softDeletedMode, pageable) + : rolloutManagement.findByRsql(rsqlParam, softDeletedMode, pageable); rest = MgmtRolloutMapper.toResponseRollout(rollouts.getContent()); } return ResponseEntity.ok(new PagedList<>(rest, rollouts.getTotalElements())); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java index 73a2a01d5..98eb356ca 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java @@ -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.util.PagingUtility; 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.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.SystemManagement; @@ -164,20 +166,19 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi { @Override public ResponseEntity> 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 Slice findModulesAll; - final long countModulesAll; + final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name()); + final Page findModulesAll; if (rsqlParam != null) { - findModulesAll = softwareModuleManagement.findByRsql(rsqlParam, pageable); - countModulesAll = ((Page) findModulesAll).getTotalElements(); + findModulesAll = softwareModuleManagement.findByRsql( + rsqlParam, softDeletedMode, pageable); } else { - findModulesAll = softwareModuleManagement.findAll(pageable); - countModulesAll = softwareModuleManagement.count(); + findModulesAll = softwareModuleManagement.findAll(softDeletedMode, pageable); } final List rest = MgmtSoftwareModuleMapper.toResponse(findModulesAll.getContent()); - return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); + return ResponseEntity.ok(new PagedList<>(rest, findModulesAll.getTotalElements())); } @Override diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResource.java index 6d0d8e6bd..85f61853c 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResource.java @@ -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.resource.mapper.MgmtSoftwareModuleTypeMapper; 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.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; @@ -45,20 +47,18 @@ public class MgmtSoftwareModuleTypeResource implements MgmtSoftwareModuleTypeRes @Override public ResponseEntity> 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 Slice findModuleTypessAll; - final long countModulesAll; + final SoftDeletedMode softDeletedMode = SoftDeletedMode.valueOf(softDeletedModeParam.name()); + final Page findModuleTypessAll; if (rsqlParam != null) { - findModuleTypessAll = softwareModuleTypeManagement.findByRsql(rsqlParam, pageable); - countModulesAll = ((Page) findModuleTypessAll).getTotalElements(); + findModuleTypessAll = softwareModuleTypeManagement.findByRsql(rsqlParam, softDeletedMode, pageable); } else { - findModuleTypessAll = softwareModuleTypeManagement.findAll(pageable); - countModulesAll = softwareModuleTypeManagement.count(); + findModuleTypessAll = softwareModuleTypeManagement.findAll(softDeletedMode, pageable); } final List rest = MgmtSoftwareModuleTypeMapper.toTypesResponse(findModuleTypessAll.getContent()); - return ResponseEntity.ok(new PagedList<>(rest, countModulesAll)); + return ResponseEntity.ok(new PagedList<>(rest, findModuleTypessAll.getTotalElements())); } @Override diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetFilterQueryMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetFilterQueryMapper.java index c2c50a842..648f3c837 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetFilterQueryMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetFilterQueryMapper.java @@ -73,7 +73,7 @@ public final class MgmtTargetFilterQueryMapper { targetRest.add( linkTo(methodOn(MgmtDistributionSetRestApi.class).getDistributionSets( "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()); } diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index efb6906a9..f19ff09f2 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -1026,6 +1026,144 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe 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. */ diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java index d04310ffb..8f6a4ce69 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java @@ -493,6 +493,142 @@ class MgmtDistributionSetTypeResourceTest extends AbstractManagementApiIntegrati 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. */ diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 59eb056f0..6e41921b6 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -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 void stopRunningRollout() throws Exception { final Rollout rollout = testdataFactory.createAndStartRollout(); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java index b4650b1d2..dd02d2c68 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java @@ -1228,6 +1228,130 @@ class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegrationTes .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}. */ diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResourceTest.java index db24362a8..3b250668b 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleTypeResourceTest.java @@ -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() { final SoftwareModuleType testType = softwareModuleTypeManagement.create(SoftwareModuleTypeManagement.Create.builder() .key("test123").name("TestName123").description("Desc123").colour("colour").maxAssignments(5).build()); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties index 858eb020e..b88939254 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties @@ -13,3 +13,8 @@ logging.level.root=WARN #logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG # Logging END 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 diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java index 9ce53a1b4..98ea48695 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java @@ -50,7 +50,7 @@ import org.springframework.security.access.prepost.PreAuthorize; * Management service for {@link DistributionSet}s. */ public interface DistributionSetManagement - extends RepositoryManagement, MetadataSupport { + extends SoftDeletableRepositoryManagement, MetadataSupport { @Override default String permissionGroup() { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java index 634f400f7..06974c048 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java @@ -37,7 +37,7 @@ import org.springframework.security.access.prepost.PreAuthorize; * Management service for {@link DistributionSetType}s. */ public interface DistributionSetTypeManagement - extends RepositoryManagement { + extends SoftDeletableRepositoryManagement { @Override default String permissionGroup() { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 2aa9e4567..d3946586f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -164,6 +164,16 @@ public interface RolloutManagement extends PermissionSupport { @PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY) Page 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 findAll(SoftDeletedMode softDeletedMode, @NotNull Pageable pageable); + /** * Get count of targets in different status in rollout. * @@ -188,6 +198,20 @@ public interface RolloutManagement extends PermissionSupport { @PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY) Page 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 findByRsql(@NotNull String rsql, SoftDeletedMode softDeletedMode, @NotNull Pageable pageable); + /** * Finds rollouts by given text in name or description. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletableRepositoryManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletableRepositoryManagement.java new file mode 100644 index 000000000..4ccd68702 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletableRepositoryManagement.java @@ -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 type of the {@link BaseEntity} + * @param type of the create request + * @param type of the update request + */ +public interface SoftDeletableRepositoryManagement> + extends RepositoryManagement { + + /** + * 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 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 findByRsql(@NotNull String rsql, @NotNull SoftDeletedMode softDeletedMode, @NotNull Pageable pageable); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletedMode.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletedMode.java new file mode 100644 index 000000000..6cb6baea0 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftDeletedMode.java @@ -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 +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java index 586489fbe..2685fb309 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java @@ -37,7 +37,7 @@ import org.springframework.security.access.prepost.PreAuthorize; * Service for managing {@link SoftwareModule}s. */ public interface SoftwareModuleManagement - extends RepositoryManagement, MetadataSupport { + extends SoftDeletableRepositoryManagement, MetadataSupport { @Override default String permissionGroup() { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java index 1b108c9f1..00be51998 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleTypeManagement.java @@ -31,7 +31,7 @@ import org.springframework.security.access.prepost.PreAuthorize; * Service for managing {@link SoftwareModuleType}s. */ public interface SoftwareModuleTypeManagement - extends RepositoryManagement { + extends SoftDeletableRepositoryManagement { @Override default String permissionGroup() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractJpaRepositoryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractJpaRepositoryManagement.java index 255a93679..2f8765e66 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractJpaRepositoryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractJpaRepositoryManagement.java @@ -38,6 +38,7 @@ import org.eclipse.hawkbit.ql.jpa.QLSupport; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.Identifiable; 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.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.jpa.Jpa; @@ -382,7 +383,13 @@ abstract class AbstractJpaRepositoryManagement>> isNotDeletedSupplier = SingletonSupplier.of(this::isNotDeleted0); - private Optional> isNotDeleted() { + protected Optional> isSoftDeleted() { + return supportSoftDelete() + ? Optional.of((root, query, cb) -> cb.equal(root.get(DELETED), true)) + : Optional.empty(); + } + + protected Optional> isNotDeleted() { return isNotDeletedSupplier.get(); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java index 02a126d45..61413784d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetManagement.java @@ -30,8 +30,10 @@ import org.eclipse.hawkbit.ql.Node; import org.eclipse.hawkbit.ql.jpa.QLSupport; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; +import org.eclipse.hawkbit.repository.Identifiable; import org.eclipse.hawkbit.repository.QuotaManagement; 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.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; @@ -105,46 +107,23 @@ public class JpaDistributionSetManagement @Override @SuppressWarnings("java:S3776") // java:S3776 - just too complex public Page findByRsql(final String rsql, final Pageable pageable) { - if (rsql != null && rsql.toLowerCase().contains(COMPLETE)) { - // limited support for 'complete' - could be removed in future - final Node node = QLSupport.getInstance().parse(rsql); - final Specification notDeleted = (root, query, cb) -> cb.equal(root.get(DELETED), false); - final List> specList = new ArrayList<>(); - specList.add(notDeleted); - final AtomicReference 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 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 findByRsqlAndDeleted(rsql, SoftDeletedMode.EXCLUDE_SOFT_DELETED, pageable); + } - return super.findByRsql(rsql, pageable); + @Override + public Page findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) { + return findByRsqlAndDeleted(rsql, softDeletedMode, pageable); + } + + @Override + public Page findAll(SoftDeletedMode softDeletedMode, Pageable pageable) { + return jpaRepository.findAll(deletedSpecification(softDeletedMode), pageable); } @Override public JpaDistributionSet update(final Update update) { + assertDistributionSetIsNotDeleted(jpaRepository.getById(update.getId())); + final JpaDistributionSet updated = super.update(update); if (Boolean.TRUE.equals(update.getLocked())) { lockSoftwareModules(updated); @@ -154,6 +133,10 @@ public class JpaDistributionSetManagement @Override public Map update(final Collection updates) { + final List ids = updates.stream().map(Identifiable::getId).toList(); + jpaRepository.findAllById(ids) + .forEach(this::assertDistributionSetIsNotDeleted); + final Map updated = super.update(updates); for (final Update update : updates) { 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) public JpaDistributionSet lock(final DistributionSet distributionSet) { final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); + assertDistributionSetIsNotDeleted(jpaDistributionSet); + if (distributionSet.isLocked()) { return jpaDistributionSet; } else { @@ -245,6 +230,8 @@ public class JpaDistributionSetManagement @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) public JpaDistributionSet unlock(final DistributionSet distributionSet) { final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); + assertDistributionSetIsNotDeleted(jpaDistributionSet); + if (jpaDistributionSet.isLocked()) { jpaDistributionSet.setLocked(false); return jpaRepository.save(jpaDistributionSet); @@ -257,6 +244,7 @@ public class JpaDistributionSetManagement @Transactional public JpaDistributionSet invalidate(final DistributionSet distributionSet) { final JpaDistributionSet jpaDistributionSet = toJpaDistributionSet(distributionSet); + assertDistributionSetIsNotDeleted(jpaDistributionSet); jpaDistributionSet.invalidate(); return jpaRepository.save(jpaDistributionSet); } @@ -296,6 +284,7 @@ public class JpaDistributionSetManagement @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) public List assignTag(final Collection ids, final long dsTagId) { return updateTag(ids, dsTagId, (tag, distributionSet) -> { + assertDistributionSetIsNotDeleted(distributionSet); if (distributionSet.getTags().contains(tag)) { return distributionSet; } else { @@ -310,6 +299,7 @@ public class JpaDistributionSetManagement @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) public List unassignTag(final Collection ids, final long dsTagId) { return updateTag(ids, dsTagId, (tag, distributionSet) -> { + assertDistributionSetIsNotDeleted(distributionSet); if (distributionSet.getTags().contains(tag)) { distributionSet.removeTag(tag); return jpaRepository.save(distributionSet); @@ -332,9 +322,6 @@ public class JpaDistributionSetManagement throw new IncompleteDistributionSetException( "Distribution set of type " + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); } - if (distributionSet.isDeleted()) { - throw new DeletedException(DistributionSet.class, id); - } return distributionSet; } @@ -384,6 +371,62 @@ public class JpaDistributionSetManagement QuotaHelper.assertAssignmentQuota(requested, maxMetaData, String.class, DistributionSet.class); } + private Page 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 deletedSpec = deletedSpecification(deletedMode); + final List> specList = new ArrayList<>(); + specList.add(deletedSpec); + final AtomicReference 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 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> specList = new ArrayList<>(); + final Specification 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 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 completeComparison) { final Node.Comparison comparison = completeComparison.get(); if (comparison.getOp() == Node.Comparison.Operator.EQ) { @@ -410,6 +453,7 @@ public class JpaDistributionSetManagement private JpaDistributionSet getValid0(final long id) { final JpaDistributionSet distributionSet = jpaRepository.getById(id); + assertDistributionSetIsNotDeleted(distributionSet); if (!distributionSet.isValid()) { throw new InvalidDistributionSetException( "Distribution set of type " + distributionSet.getType().getKey() + " is invalid: " + distributionSet.getId()); @@ -473,4 +517,10 @@ public class JpaDistributionSetManagement throw new EntityNotFoundException(DistributionSetTag.class, tagId); } } + + private void assertDistributionSetIsNotDeleted(final JpaDistributionSet jpaDistributionSet) { + if (jpaDistributionSet.isDeleted()) { + throw new DeletedException(DistributionSet.class, jpaDistributionSet.getId()); + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetTypeManagement.java index 4ecfd9cd5..5e352b87e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDistributionSetTypeManagement.java @@ -11,15 +11,21 @@ package org.eclipse.hawkbit.repository.jpa.management; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import jakarta.persistence.EntityManager; +import org.eclipse.hawkbit.ql.jpa.QLSupport; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.Identifiable; 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.DeletedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; 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.configuration.Constants; 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.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.transaction.annotation.Transactional; @@ -48,6 +57,7 @@ public class JpaDistributionSetTypeManagement extends AbstractJpaRepositoryManagement implements DistributionSetTypeManagement { + private final DistributionSetTypeRepository distributionSetTypeRepository; private final SoftwareModuleTypeRepository softwareModuleTypeRepository; private final DistributionSetRepository distributionSetRepository; private final TargetTypeRepository targetTypeRepository; @@ -60,6 +70,7 @@ public class JpaDistributionSetTypeManagement final DistributionSetRepository distributionSetRepository, final TargetTypeRepository targetTypeRepository, final QuotaManagement quotaManagement) { super(distributionSetTypeRepository, entityManager); + this.distributionSetTypeRepository = distributionSetTypeRepository; this.softwareModuleTypeRepository = softwareModuleTypeRepository; this.distributionSetRepository = distributionSetRepository; this.targetTypeRepository = targetTypeRepository; @@ -117,11 +128,53 @@ public class JpaDistributionSetTypeManagement @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) public JpaDistributionSetType unassignSoftwareModuleType(final long id, final long softwareModuleTypeId) { final JpaDistributionSetType type = jpaRepository.getById(id); + assertDistributionSetTypeIsNotDeleted(type); checkDistributionSetTypeNotAssigned(id); type.removeModuleType(softwareModuleTypeRepository.getById(softwareModuleTypeId)); return jpaRepository.save(type); } + @Override + public Page findAll(SoftDeletedMode softDeletedMode, Pageable pageable) { + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification deletedSpec = + DistributionSetTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED); + + return distributionSetTypeRepository.findAll(deletedSpec, pageable); + } + return distributionSetTypeRepository.findAll(pageable); + } + + @Override + public Page findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) { + final Specification rsqlSpec = QLSupport.getInstance().buildSpec(rsql, DistributionSetTypeFields.class); + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification 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 update(final Collection updates) { + final List ids = updates.stream().map(Identifiable::getId).toList(); + distributionSetTypeRepository.findAllById(ids).forEach(this::assertDistributionSetTypeIsNotDeleted); + return super.update(updates); + } + private JpaDistributionSetType assignSoftwareModuleTypes( final long dsTypeId, final Collection softwareModulesTypeIds, final boolean mandatory) { final Collection foundModules = softwareModuleTypeRepository.findAllById(softwareModulesTypeIds); @@ -132,6 +185,7 @@ public class JpaDistributionSetTypeManagement final JpaDistributionSetType type = jpaRepository.getById(dsTypeId); + assertDistributionSetTypeIsNotDeleted(type); checkDistributionSetTypeNotAssigned(dsTypeId); 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)); } } + + private void assertDistributionSetTypeIsNotDeleted(final DistributionSetType distributionSetType){ + if (distributionSetType.isDeleted()) { + throw new DeletedException(DistributionSetType.class, distributionSetType.getId()); + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java index e5f1ab5e5..49f19cab8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java @@ -41,6 +41,7 @@ import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutHelper; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutStatusCache; +import org.eclipse.hawkbit.repository.SoftDeletedMode; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.event.EventPublisherHolder; 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); } + @Override + public Page 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 public Page findAllWithDetailedStatus(final boolean deleted, final Pageable pageable) { return appendStatusDetails(JpaManagementHelper.convertPage( @@ -281,6 +294,21 @@ public class JpaRolloutManagement implements RolloutManagement { return JpaManagementHelper.convertPage(rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(specList), pageable), pageable); } + @Override + public Page findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) { + final Specification rsqlSpec = QLSupport.getInstance().buildSpec(rsql, RolloutFields.class); + + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification 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 public Page findByRsqlWithDetailedStatus(final String rsql, final boolean deleted, final Pageable pageable) { final List> specList = List.of( diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java index 3925fefed..2f893a86c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleManagement.java @@ -22,9 +22,13 @@ import java.util.stream.Collectors; import jakarta.persistence.EntityManager; 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.Identifiable; import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.SoftDeletedMode; 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.IncompleteSoftwareModuleException; 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.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.transaction.annotation.Transactional; @@ -56,6 +61,7 @@ public class JpaSoftwareModuleManagement extends AbstractJpaRepositoryWithMetadataManagement implements SoftwareModuleManagement { + private final SoftwareModuleRepository softwareModuleRepository; private final DistributionSetRepository distributionSetRepository; private final ArtifactManagement artifactManagement; private final QuotaManagement quotaManagement; @@ -64,6 +70,7 @@ public class JpaSoftwareModuleManagement extends final DistributionSetRepository distributionSetRepository, final ArtifactManagement artifactManagement, final QuotaManagement quotaManagement) { super(softwareModuleRepository, entityManager); + this.softwareModuleRepository = softwareModuleRepository; this.distributionSetRepository = distributionSetRepository; this.artifactManagement = artifactManagement; this.quotaManagement = quotaManagement; @@ -96,6 +103,25 @@ public class JpaSoftwareModuleManagement extends 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 update(final Collection updates) { + final List ids = updates.stream().map(Identifiable::getId).toList(); + + softwareModuleRepository.findAllById(ids).forEach(this::assertSoftwareModuleIsNotDeleted); + return super.update(updates); + } + @Override protected List softDelete(final Collection toDelete) { 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) public JpaSoftwareModule lock(final SoftwareModule softwareModule) { final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule); + assertSoftwareModuleIsNotDeleted(jpaSoftwareModule); if (jpaSoftwareModule.isLocked()) { return jpaSoftwareModule; } else { @@ -154,6 +181,7 @@ public class JpaSoftwareModuleManagement extends @Retryable(includes = ConcurrencyFailureException.class, maxRetriesString = Constants.RETRY_MAX, delayString = Constants.RETRY_DELAY) public JpaSoftwareModule unlock(final SoftwareModule softwareModule) { final JpaSoftwareModule jpaSoftwareModule = toJpaSoftwareModule(softwareModule); + assertSoftwareModuleIsNotDeleted(jpaSoftwareModule); if (softwareModule.isLocked()) { jpaSoftwareModule.unlock(); return jpaRepository.save(jpaSoftwareModule); @@ -162,6 +190,30 @@ public class JpaSoftwareModuleManagement extends } } + @Override + public Page findAll(SoftDeletedMode softDeletedMode, Pageable pageable) { + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + Specification softDeletedSpec = + SoftwareModuleSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED); + return softwareModuleRepository.findAll(softDeletedSpec, pageable); + } + + return softwareModuleRepository.findAll(pageable); + } + + @Override + public Page findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) { + final Specification rsqlSpec = QLSupport.getInstance().buildSpec(rsql, SoftwareModuleFields.class); + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification softDeletedSpec = + SoftwareModuleSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED); + return softwareModuleRepository.findAll( + JpaManagementHelper.combineWithAnd(List.of(rsqlSpec, softDeletedSpec)), pageable); + } + + return softwareModuleRepository.findAll(rsqlSpec, pageable); + } + @Override public Page findByAssignedTo(final long distributionSetId, final Pageable pageable) { assertDistributionSetExists(distributionSetId); @@ -201,4 +253,10 @@ public class JpaSoftwareModuleManagement extends throw new EntityNotFoundException(DistributionSet.class, distributionSetId); } } + + private void assertSoftwareModuleIsNotDeleted(final SoftwareModule softwareModule) { + if (softwareModule.isDeleted()) { + throw new DeletedException(SoftwareModule.class, softwareModule.getId()); + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleTypeManagement.java index f999fb11f..dcf09e372 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaSoftwareModuleTypeManagement.java @@ -10,20 +10,36 @@ package org.eclipse.hawkbit.repository.jpa.management; import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.Optional; 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.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.repository.DistributionSetTypeRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository; 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.tenancy.TenantAwareCacheManager; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; 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.transaction.annotation.Transactional; @Service @ConditionalOnBooleanProperty(prefix = "hawkbit.jpa", name = { "enabled", "software-module-type=management" }, matchIfMissing = true) @@ -31,6 +47,7 @@ public class JpaSoftwareModuleTypeManagement extends AbstractJpaRepositoryManagement implements SoftwareModuleTypeManagement { + private final SoftwareModuleTypeRepository softwareModuleTypeRepository; private final DistributionSetTypeRepository distributionSetTypeRepository; private final SoftwareModuleRepository softwareModuleRepository; @@ -40,6 +57,7 @@ public class JpaSoftwareModuleTypeManagement final DistributionSetTypeRepository distributionSetTypeRepository, final SoftwareModuleRepository softwareModuleRepository) { super(softwareModuleTypeRepository, entityManager); + this.softwareModuleTypeRepository = softwareModuleTypeRepository; this.distributionSetTypeRepository = distributionSetTypeRepository; this.softwareModuleRepository = softwareModuleRepository; } @@ -54,9 +72,56 @@ public class JpaSoftwareModuleTypeManagement return jpaRepository.findByKey(key); } + @Override + public Page findAll(SoftDeletedMode softDeletedMode, Pageable pageable) { + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification deletedSpec = + SoftwareModuleTypeSpecification.isDeleted(softDeletedMode == SoftDeletedMode.ONLY_SOFT_DELETED); + + return softwareModuleTypeRepository.findAll(deletedSpec, pageable); + } + return softwareModuleTypeRepository.findAll(pageable); + } + + @Override + public Page findByRsql(String rsql, SoftDeletedMode softDeletedMode, Pageable pageable) { + final Specification rsqlSpec = QLSupport.getInstance().buildSpec(rsql, SoftwareModuleTypeFields.class); + if (softDeletedMode != SoftDeletedMode.INCLUDE_SOFT_DELETED) { + final Specification 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 update(final Collection updates) { + final List ids = updates.stream().map(Identifiable::getId).toList(); + softwareModuleTypeRepository.findAllById(ids).forEach(this::assertSoftwareModuleTypeIsNotDeleted); + return super.update(updates); + } + @Override protected Collection softDelete(final Collection toDelete) { return toDelete.stream().filter(smt -> 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()); + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetTypeSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetTypeSpecification.java index 16aa6cd09..e64cf852f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetTypeSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetTypeSpecification.java @@ -13,6 +13,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; 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.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.springframework.data.jpa.domain.Specification; @@ -34,4 +35,9 @@ public final class DistributionSetTypeSpecification { public static Specification byKey(final String key) { return (targetRoot, query, cb) -> cb.equal(targetRoot.get(AbstractJpaTypeEntity_.key), key); } + + public static Specification isDeleted(final Boolean isDeleted) { + return (root, query, cb) -> + cb.equal(root. get(JpaDistributionSetType_.deleted), isDeleted); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java index 0c66f7a9a..790991c83 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java @@ -32,4 +32,9 @@ public final class SoftwareModuleSpecification { return cb.equal(join.get(AbstractJpaBaseEntity_.ID), dsId); }; } + + public static Specification isDeleted(final Boolean isDeleted) { + return (root, query, cb) -> + cb.equal(root. get(JpaSoftwareModule_.deleted), isDeleted); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleTypeSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleTypeSpecification.java new file mode 100644 index 000000000..3513d000d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleTypeSpecification.java @@ -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 isDeleted(final Boolean isDeleted) { + return (root, query, cb) -> + cb.equal(root. get(JpaSoftwareModuleType_.deleted), isDeleted); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/AbstractRepositoryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/AbstractRepositoryManagementTest.java index 62c790a95..9d769bf09 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/AbstractRepositoryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/AbstractRepositoryManagementTest.java @@ -457,16 +457,16 @@ public abstract class AbstractRepositoryManagementTest inInterfaces = Arrays.stream(resolvableType.getInterfaces()) - .filter(superInterface -> superInterface.getRawClass() == targetSuperClass) - .findAny(); - if (inInterfaces.isPresent()) { - return inInterfaces; - } else if (resolvableType.getSuperType() != ResolvableType.NONE) { - return findGenericSuperType(resolvableType.getSuperType(), targetSuperClass); - } else { - return Optional.empty(); + for (final ResolvableType superInterface : resolvableType.getInterfaces()) { + final Optional found = findGenericSuperType(superInterface, targetSuperClass); + if (found.isPresent()) { + return found; + } } + if (resolvableType.getSuperType() != ResolvableType.NONE) { + return findGenericSuperType(resolvableType.getSuperType(), targetSuperClass); + } + return Optional.empty(); } private void assertEquals(final Object actual, final Object expected, final Deque path) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java index 663d083e5..13362111f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement.Update; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.Identifiable; +import org.eclipse.hawkbit.repository.exception.DeletedException; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; @@ -936,4 +937,19 @@ class DistributionSetManagementTest extends AbstractRepositoryManagementWithMeta .as("entity with too short version should not be updated") .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 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); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java index 8f0fd97cf..16ae799e6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Create; import org.eclipse.hawkbit.repository.SoftwareModuleManagement.Update; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; 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.LockedException; 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 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) { for (final Artifact result : results) { final String currentTenant = AccessContext.tenant(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java index 75c440930..3fe7f02dd 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java @@ -95,7 +95,7 @@ public class DistributionSetView extends TableView { (query, rsqlFilter) -> Optional.ofNullable( hawkbitClient.getDistributionSetRestApi() .getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query - .getSortOrders())) + .getSortOrders()), null) .getBody()) .stream().flatMap(body -> body.getContent().stream()), e -> new CreateDialog(hawkbitClient).result(), @@ -158,7 +158,7 @@ public class DistributionSetView extends TableView { type.setItemLabelGenerator(MgmtDistributionSetType::getName); type.setItems(Optional.ofNullable( hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC) + .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC, null) .getBody()) .map(PagedList::getContent) .orElseGet(Collections::emptyList)); @@ -266,7 +266,7 @@ public class DistributionSetView extends TableView { this::readyToCreate, Optional.ofNullable( hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC) + .getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC, null) .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .orElseGet(() -> new MgmtDistributionSetType[0])); @@ -353,6 +353,8 @@ public class DistributionSetView extends TableView { v -> new Utils.BaseDialog("Add Software Modules") { { + setHeight("80vh"); + setWidth("80vw"); final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient); add(softwareModulesView); final Button addBtn = new Button("Add"); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java index ccd65ab85..19ab95fe5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java @@ -96,7 +96,7 @@ public final class RolloutView extends TableView (query, rsqlFilter) -> Optional.ofNullable( hawkbitClient.getRolloutRestApi() .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()), selectionGrid -> new CreateDialog(hawkbitClient).result(), selectionGrid -> { @@ -344,7 +344,7 @@ public final class RolloutView extends TableView "Distribution Set", this::readyToCreate, 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()); distributionSet.setRequiredIndicatorVisible(true); distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java index 848d44401..fe16369cf 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java @@ -98,7 +98,8 @@ public class SoftwareModuleView extends TableView { }, (query, rsqlFilter) -> Optional.ofNullable( hawkbitClient.getSoftwareModuleRestApi() - .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) + .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC, + null) .getBody()) .stream().map(PagedList::getContent).flatMap(List::stream), isParent ? v -> new CreateDialog(hawkbitClient).result() : null, @@ -137,7 +138,7 @@ public class SoftwareModuleView extends TableView { type.setItemLabelGenerator(MgmtSoftwareModuleType::getName); type.setItems(Optional.ofNullable( hawkbitClient.getSoftwareModuleTypeRestApi() - .getTypes(null, 0, 20, Constants.NAME_ASC) + .getTypes(null, 0, 20, Constants.NAME_ASC, null) .getBody()) .map(PagedList::getContent) .orElseGet(Collections::emptyList)); @@ -231,7 +232,7 @@ public class SoftwareModuleView extends TableView { this::readyToCreate, Optional.ofNullable( hawkbitClient.getSoftwareModuleTypeRestApi() - .getTypes(null, 0, 30, Constants.NAME_ASC) + .getTypes(null, 0, 30, Constants.NAME_ASC, null) .getBody()) .map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0])) .orElseGet(() -> new MgmtSoftwareModuleType[0])); @@ -264,7 +265,7 @@ public class SoftwareModuleView extends TableView { distType.setItems( Optional.ofNullable( hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) + .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC, null) .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .orElseGet(() -> new MgmtDistributionSetType[0])); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java index 8e34f75df..3eec3c140 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java @@ -256,7 +256,8 @@ public class TargetFilterQueryView extends TableView body.getContent().stream())); distributionSet.setItemLabelGenerator(ds -> ds.getName() + ":" + ds.getVersion()); distributionSet.focus(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java index 82111ac18..f22269f3a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java @@ -869,7 +869,7 @@ public final class TargetView extends TableView "Distribution Set", this::readyToAssign, 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()); distributionSet.setRequiredIndicatorVisible(true); distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());