From 3fa1dd1be489631d7653d9f5835f87274943c64e Mon Sep 17 00:00:00 2001 From: Anand Kumar Date: Wed, 25 Aug 2021 12:13:27 +0200 Subject: [PATCH] Feature target type entity (#1162) * Added Target Type model Signed-off-by: Anand kumar * Added Target Type JPA model Signed-off-by: Anand kumar * Added Target Type repository model classes Signed-off-by: Anand kumar * Removed the name entity from Target Type Signed-off-by: Anand kumar * Refactored the Target Type models Signed-off-by: Anand kumar * Added the DB migration script and updated the Target Type models Signed-off-by: Anand kumar * Added target type in target Mapper Signed-off-by: Anand kumar * Changed the target type ID to Long Signed-off-by: Anand kumar * Added MYSQL DB migration script and removed the deleted column for target type Signed-off-by: Anand kumar * Updated the DB migration script for target table Signed-off-by: Anand kumar * Added manyToMany reltation between target type and Ds type Signed-off-by: Anand kumar * Added POSTGRESQL DB migration script Signed-off-by: Anand kumar * Added MSSQL SERVER DB migration script Signed-off-by: Anand kumar * Added DB2 DB migration script Signed-off-by: Anand kumar * Added missing license header and java docs Signed-off-by: Anand kumar * Added on delete cascade in DB migration script Signed-off-by: Anand kumar * Added Target Type specification Signed-off-by: Anand kumar * Removed the delete cascade and Added type API Signed-off-by: Anand kumar * fixed API doc build Signed-off-by: Anand kumar * Added target type management test Signed-off-by: Anand kumar * Added target type events test Signed-off-by: Anand kumar * Added target type update and unassign to target Signed-off-by: Anand kumar * Added API tests for assigning target type to target Signed-off-by: Anand kumar * Added missing license header Signed-off-by: Anand kumar * Added missing docs Signed-off-by: Anand kumar * Fixed sonar issues Signed-off-by: Anand kumar * Fixed license header build issue Signed-off-by: Anand kumar * Updated the attribute name to target type Signed-off-by: Anand kumar * Fixed the review comments Signed-off-by: Anand kumar * Removed unused error status variable Signed-off-by: Anand kumar * Added target API to assign target type Signed-off-by: Anand kumar * Added the tests for assigning target type to target Signed-off-by: Anand kumar * Fixed the review comments for null check Signed-off-by: Anand kumar --- docs/content/apis/management_api.md | 1 + docs/content/apis/mgmt/targettypes.md | 7 + .../hawkbit/exception/SpServerError.java | 4 +- .../hawkbit/repository/TargetTypeFields.java | 40 ++ .../hawkbit/repository/EntityFactory.java | 6 + .../hawkbit/repository/QuotaManagement.java | 5 + .../hawkbit/repository/TargetManagement.java | 25 + .../repository/TargetTypeManagement.java | 166 +++++ .../repository/builder/TargetCreate.java | 7 + .../repository/builder/TargetTypeBuilder.java | 29 + .../repository/builder/TargetTypeCreate.java | 81 +++ .../repository/builder/TargetTypeUpdate.java | 42 ++ .../repository/builder/TargetUpdate.java | 7 + .../event/remote/TargetTypeDeletedEvent.java | 46 ++ .../remote/entity/TargetTypeCreatedEvent.java | 41 ++ .../remote/entity/TargetTypeUpdatedEvent.java | 42 ++ ...butionSetTypeNotInTargetTypeException.java | 37 ++ .../exception/TargetTypeInUseException.java | 61 ++ .../hawkbit/repository/model/Target.java | 5 + .../hawkbit/repository/model/TargetType.java | 69 ++ .../repository/PropertiesQuotaManagement.java | 4 + .../AbstractTargetTypeUpdateCreate.java | 63 ++ .../builder/AbstractTargetUpdateCreate.java | 14 + .../builder/GenericTargetTypeUpdate.java | 25 + .../jpa/JpaDistributionSetTypeManagement.java | 22 +- .../repository/jpa/JpaEntityFactory.java | 14 +- .../repository/jpa/JpaSystemManagement.java | 5 + .../repository/jpa/JpaTargetManagement.java | 33 +- .../jpa/JpaTargetTypeManagement.java | 250 +++++++ .../RepositoryApplicationConfiguration.java | 43 +- .../repository/jpa/TargetRepository.java | 10 + .../repository/jpa/TargetTypeRepository.java | 186 ++++++ .../jpa/builder/JpaTargetBuilder.java | 13 +- .../jpa/builder/JpaTargetCreate.java | 20 +- .../jpa/builder/JpaTargetTypeBuilder.java | 45 ++ .../jpa/builder/JpaTargetTypeCreate.java | 65 ++ .../jpa/model/JpaDistributionSetType.java | 41 +- .../repository/jpa/model/JpaTarget.java | 95 ++- .../repository/jpa/model/JpaTargetType.java | 156 +++++ .../TargetTypeSpecification.java | 149 +++++ .../DB2/V1_12_18__add_target_type___DB2.sql | 32 + .../H2/V1_12_18__add_target_type___H2.sql | 50 ++ .../V1_12_18__add_target_type___MYSQL.sql | 47 ++ ...V1_12_18__add_target_type___POSTGRESQL.sql | 65 ++ ...V1_12_18__add_target_type___SQL_SERVER.sql | 27 + .../jpa/AbstractJpaIntegrationTest.java | 3 + .../repository/jpa/TargetManagementTest.java | 64 ++ .../jpa/TargetTypeManagementTest.java | 230 +++++++ .../jpa/event/RepositoryEntityEventTest.java | 37 ++ .../test/util/AbstractIntegrationTest.java | 4 + .../util/JpaTestRepositoryManagement.java | 2 +- .../repository/test/util/TestdataFactory.java | 74 +++ .../MgmtDistributionSetTypeAssignment.java | 20 + .../mgmt/json/model/target/MgmtTarget.java | 24 + .../model/target/MgmtTargetRequestBody.java | 38 ++ .../json/model/targettype/MgmtTargetType.java | 61 ++ .../MgmtTargetTypeRequestBodyPost.java | 69 ++ .../MgmtTargetTypeRequestBodyPut.java | 79 +++ .../mgmt/rest/api/MgmtRestConstants.java | 20 + .../mgmt/rest/api/MgmtTargetRestApi.java | 26 + .../mgmt/rest/api/MgmtTargetTypeRestApi.java | 159 +++++ .../MgmtDistributionSetTypeMapper.java | 2 +- .../mgmt/rest/resource/MgmtTargetMapper.java | 11 +- .../rest/resource/MgmtTargetResource.java | 16 +- .../rest/resource/MgmtTargetTypeMapper.java | 81 +++ .../rest/resource/MgmtTargetTypeResource.java | 148 +++++ .../mgmt/rest/resource/PagingUtility.java | 9 + .../rest/resource/MgmtTargetResourceTest.java | 193 ++++++ .../resource/MgmtTargetTypeResourceTest.java | 616 ++++++++++++++++++ .../exception/ResponseExceptionHandler.java | 1 + .../hawkbit/rest/util/JsonBuilder.java | 84 ++- .../main/asciidoc/targettypes-api-guide.adoc | 396 +++++++++++ .../ApiModelPropertiesGeneric.java | 6 +- .../documentation/MgmtApiModelProperties.java | 6 + .../TargetTypesDocumentationTest.java | 273 ++++++++ .../security/HawkbitSecurityProperties.java | 9 + 76 files changed, 4915 insertions(+), 41 deletions(-) create mode 100644 docs/content/apis/mgmt/targettypes.md create mode 100644 hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetTypeFields.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeBuilder.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeCreate.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeUpdate.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetTypeDeletedEvent.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeCreatedEvent.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeUpdatedEvent.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeNotInTargetTypeException.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TargetTypeInUseException.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetType.java create mode 100644 hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetTypeUpdateCreate.java create mode 100644 hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericTargetTypeUpdate.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTypeManagement.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeBuilder.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeCreate.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetType.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetTypeSpecification.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_18__add_target_type___DB2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_18__add_target_type___H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_18__add_target_type___MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_18__add_target_type___POSTGRESQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_18__add_target_type___SQL_SERVER.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetTypeManagementTest.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionsettype/MgmtDistributionSetTypeAssignment.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetType.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPost.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPut.java create mode 100644 hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetTypeRestApi.java create mode 100644 hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeMapper.java create mode 100644 hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResource.java create mode 100644 hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResourceTest.java create mode 100644 hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targettypes-api-guide.adoc create mode 100644 hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetTypesDocumentationTest.java diff --git a/docs/content/apis/management_api.md b/docs/content/apis/management_api.md index 7062eaa6d..d7497c922 100644 --- a/docs/content/apis/management_api.md +++ b/docs/content/apis/management_api.md @@ -29,6 +29,7 @@ Supported HTTP-methods are: Available Management APIs resources are: - [Targets](/hawkbit/apis/mgmt/targets/) +- [Target types](/hawkbit/apis/mgmt/targettypess/) - [Distribution sets](/hawkbit/apis/mgmt/distributionsets/) - [Distribution set types](/hawkbit/apis/mgmt/distributionsettypes/) - [Software modules](/hawkbit/apis/mgmt/softwaremodules/) diff --git a/docs/content/apis/mgmt/targettypes.md b/docs/content/apis/mgmt/targettypes.md new file mode 100644 index 000000000..319d5c65e --- /dev/null +++ b/docs/content/apis/mgmt/targettypes.md @@ -0,0 +1,7 @@ +--- +title: Target Types API +parent: Management API +weight: -100 +--- + + \ No newline at end of file diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index 5f082142d..9840f4dab 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -275,7 +275,9 @@ public enum SpServerError { * */ SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE("hawkbit.server.error.noWeightProvidedInMultiAssignmentMode", - "The requested operation requires a weight to be specified when multi assignments is enabled."); + "The requested operation requires a weight to be specified when multi assignments is enabled."), + + SP_TARGET_TYPE_IN_USE("hawkbit.server.error.target.type.used", "Target type is still in use by a target."); private final String key; private final String message; diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetTypeFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetTypeFields.java new file mode 100644 index 000000000..54cf9c454 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetTypeFields.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +/** + * Describing the fields of the TargetType model which can be used in + * the REST API + */ +public enum TargetTypeFields implements FieldNameProvider { + /** + * The name field. + */ + NAME("name"), + /** + * The description field. + */ + DESCRIPTION("description"), + + /** + * The id field. + */ + ID("id"); + + private final String fieldName; + + TargetTypeFields(final String fieldName) { + this.fieldName = fieldName; + } + + @Override + public String getFieldName() { + return fieldName; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java index 65fe29c69..b5fb5c390 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/EntityFactory.java @@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.builder.SoftwareModuleTypeBuilder; import org.eclipse.hawkbit.repository.builder.TagBuilder; import org.eclipse.hawkbit.repository.builder.TargetBuilder; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryBuilder; +import org.eclipse.hawkbit.repository.builder.TargetTypeBuilder; import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.MetaData; @@ -109,6 +110,11 @@ public interface EntityFactory { */ TargetBuilder target(); + /** + * @return {@link TargetTypeBuilder} object + */ + TargetTypeBuilder targetType(); + /** * @return {@link TargetFilterQueryBuilder} object */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java index 378801d02..ce965854d 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java @@ -106,4 +106,9 @@ public interface QuotaManagement { */ long getMaxArtifactStorage(); + /** + * @return the maximum number of distribution set types per target type + */ + int getMaxDistributionSetTypesPerTargetType(); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java index 7194fab9e..c78a44a03 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetManagement.java @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.springframework.data.domain.Page; @@ -598,6 +599,30 @@ public interface TargetManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) Target unAssignTag(@NotEmpty String controllerID, long targetTagId); + /** + * Un-assign a {@link TargetType} assignment to given {@link Target}. + * + * @param controllerID + * to un-assign for + * @return the unassigned target + * + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + Target unAssignType(@NotEmpty String controllerID); + + /** + * Assign a {@link TargetType} assignment to given {@link Target}. + * + * @param controllerID + * to un-assign for + * @return the unassigned target + * + * @throws EntityNotFoundException + * if TargetType with given target ID does not exist + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + Target assignType(@NotEmpty String controllerID, @NotNull long targetTypeId); + /** * updates the {@link Target}. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java new file mode 100644 index 000000000..8bab106f4 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.security.access.prepost.PreAuthorize; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * Management service for {@link TargetType}s. + * + */ +public interface TargetTypeManagement { + + /** + * @param name + * as {@link TargetType#getName()} + * @return {@link TargetType} + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Optional getByName(@NotEmpty String name); + + /** + * @return total count + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + long count(); + + /** + * @param create + * TargetTypeCreate + * @return targetType + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + TargetType create(@NotNull @Valid TargetTypeCreate create); + + /** + * @param creates + * List of TargetTypeCreate + * @return List of targetType + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + List create(@NotEmpty @Valid Collection creates); + + /** + * @param id targetTypeId + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_DELETE_TARGET) + void delete(@NotNull Long id); + + /** + * @param pageable + * Page + * @return TargetType page + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Page findAll(@NotNull Pageable pageable); + + /** + * @param pageable + * Page + * @param rsqlParam + * query param + * @return Target type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Page findByRsql(@NotNull Pageable pageable, @NotEmpty String rsqlParam); + + /** + * @param id + * Target type ID + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Optional get(long id); + + /** + * @param targetId + * Target ID + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Optional findByTargetId(long targetId); + + /** + * @param targetIds + * List of Target ID + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + List findByTargetIds(Collection targetIds); + + /** + * @param controllerId + * Target controller ID + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Optional findByTargetControllerId(String controllerId); + + /** + * @param controllerIds + * List of Target controller ID + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + List findByTargetControllerIds(Collection controllerIds); + + + /** + * @param ids + * List of Target type ID + * @return Target type list + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + List get(@NotEmpty Collection ids); + + + /** + * @param update + * TargetTypeUpdate + * @return Target Type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + TargetType update(@NotNull @Valid TargetTypeUpdate update); + + /** + * @param targetTypeId + * Target type ID + * @param distributionSetTypeIds + * Distribution set ID + * @return Target type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + TargetType assignCompatibleDistributionSetTypes(long targetTypeId, + @NotEmpty Collection distributionSetTypeIds); + + /** + * @param targetTypeId + * Target type ID + * @param distributionSetTypeIds + * Distribution set ID + * @return Target type + */ + @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) + TargetType unassignDistributionSetType(long targetTypeId, long distributionSetTypeIds); + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetCreate.java index b787f97da..9af746391 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetCreate.java @@ -46,6 +46,13 @@ public interface TargetCreate { */ TargetCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); + /** + * @param targetTypeId + * for {@link Target#getTargetType()} + * @return updated builder instance + */ + TargetCreate targetType(Long targetTypeId); + /** * @param securityToken * for {@link Target#getSecurityToken()} is generated with a diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeBuilder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeBuilder.java new file mode 100644 index 000000000..fa17a67e8 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeBuilder.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.builder; + +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * Builder for {@link TargetType}. + * + */ +public interface TargetTypeBuilder { + /** + * @param id + * of the updatable entity + * @return builder instance + */ + TargetTypeUpdate update(long id); + + /** + * @return builder instance + */ + TargetTypeCreate create(); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeCreate.java new file mode 100644 index 000000000..c6770c655 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeCreate.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.builder; + +import org.eclipse.hawkbit.repository.model.BaseEntity; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.TargetType; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Collection; +import java.util.Collections; + +/** + * Builder to create a new {@link TargetType} entry. Defines all fields + * that can be set at creation time. Other fields are set by the repository + * automatically, e.g. {@link BaseEntity#getCreatedAt()}. + * + */ +public interface TargetTypeCreate { + + /** + * @param name + * for {@link TargetType#getName()} + * @return updated builder instance + */ + TargetTypeCreate name(@Size(min = 1, max = NamedEntity.NAME_MAX_SIZE) @NotEmpty String name); + + /** + * @param description + * for {@link TargetType#getDescription()} + * @return updated builder instance + */ + TargetTypeCreate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); + + /** + * @param colour + * for {@link TargetType#getColour()} + * @return updated builder instance + */ + TargetTypeCreate colour(@Size(max = TargetType.COLOUR_MAX_SIZE) String colour); + + /** + * @param compatible + * for {@link TargetType#getCompatibleDistributionSetTypes()} + * @return updated builder instance + */ + TargetTypeCreate compatible(@NotEmpty Collection compatible); + + /** + * @param compatible + * for {@link TargetType#getCompatibleDistributionSetTypes()} + * @return updated builder instance + */ + default TargetTypeCreate compatible(@NotNull final Long compatible) { + return compatible(Collections.singletonList(compatible)); + } + + /** + * @param compatible + * for {@link TargetType#getCompatibleDistributionSetTypes()} + * @return updated builder instance + */ + default TargetTypeCreate compatible(@NotNull final DistributionSetType compatible) { + return compatible(compatible.getId()); + } + + /** + * @return peek on current state of {@link TargetType} in the + * builder + */ + TargetType build(); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeUpdate.java new file mode 100644 index 000000000..dca751247 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetTypeUpdate.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.builder; + +import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.TargetType; + +import javax.validation.constraints.Size; + +/** + * Builder to update an existing {@link TargetType} entry. Defines all + * fields that can be updated. + * + */ +public interface TargetTypeUpdate { + /** + * @param description + * for {@link TargetType#getDescription()} + * @return updated builder instance + */ + TargetTypeUpdate description(@Size(max = NamedEntity.DESCRIPTION_MAX_SIZE) String description); + + /** + * @param colour + * for {@link TargetType#getColour()} + * @return updated builder instance + */ + TargetTypeUpdate colour(@Size(max = TargetType.COLOUR_MAX_SIZE) String colour); + + /** + * @param name + * Name + * @return updated builder instance + */ + TargetTypeUpdate name(@Size(max = TargetType.NAME_MAX_SIZE) String name); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetUpdate.java index 76154b2ca..f7fc03c5f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetUpdate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/TargetUpdate.java @@ -43,6 +43,13 @@ public interface TargetUpdate { */ TargetUpdate securityToken(@Size(min = 1, max = Target.SECURITY_TOKEN_MAX_SIZE) @NotNull String securityToken); + /** + * @param targetTypeId + * for {@link Target#getTargetType()} + * @return updated builder instance + */ + TargetUpdate targetType(@NotNull Long targetTypeId); + /** * @param address * for {@link Target#getAddress()} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetTypeDeletedEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetTypeDeletedEvent.java new file mode 100644 index 000000000..d411773bd --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetTypeDeletedEvent.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.event.remote; + +import org.eclipse.hawkbit.repository.event.entity.EntityDeletedEvent; +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * + * Defines the remote event of deleting a {@link TargetType}. + */ +public class TargetTypeDeletedEvent extends RemoteIdEvent implements EntityDeletedEvent { + + private static final long serialVersionUID = 1L; + + /** + * Default constructor. + */ + public TargetTypeDeletedEvent() { + // for serialization libs like jackson + } + + /** + * Constructor for json serialization. + * + * @param tenant + * the tenant + * @param entityId + * the entity id + * @param entityClass + * the entity class + * @param applicationId + * the origin application id + */ + public TargetTypeDeletedEvent(final String tenant, final Long entityId, final String entityClass, + final String applicationId) { + super(entityId, tenant, entityClass, applicationId); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeCreatedEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeCreatedEvent.java new file mode 100644 index 000000000..12c917b5c --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeCreatedEvent.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.event.remote.entity; + +import org.eclipse.hawkbit.repository.event.entity.EntityCreatedEvent; +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * Defines the remote event of creating a new {@link TargetType}. + * + */ +public class TargetTypeCreatedEvent extends RemoteEntityEvent + implements EntityCreatedEvent { + private static final long serialVersionUID = 1L; + + /** + * Default constructor. + */ + public TargetTypeCreatedEvent() { + // for serialization libs like jackson + } + + /** + * Constructor. + * + * @param baseEntity + * the TargetType + * @param applicationId + * the origin application id + */ + public TargetTypeCreatedEvent(final TargetType baseEntity, final String applicationId) { + super(baseEntity, applicationId); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeUpdatedEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeUpdatedEvent.java new file mode 100644 index 000000000..f001cb25a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTypeUpdatedEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.event.remote.entity; + +import org.eclipse.hawkbit.repository.event.entity.EntityUpdatedEvent; +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * Defines the remote event for updating a {@link TargetType}. + * + */ +public class TargetTypeUpdatedEvent extends RemoteEntityEvent + implements EntityUpdatedEvent { + + private static final long serialVersionUID = 1L; + + /** + * Default constructor. + */ + public TargetTypeUpdatedEvent() { + // for serialization libs like jackson + } + + /** + * Constructor. + * + * @param baseEntity + * TargetType + * @param applicationId + * the origin application id + */ + public TargetTypeUpdatedEvent(final TargetType baseEntity, final String applicationId) { + super(baseEntity, applicationId); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeNotInTargetTypeException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeNotInTargetTypeException.java new file mode 100644 index 000000000..d44376900 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/DistributionSetTypeNotInTargetTypeException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * the {@link DistributionSetTypeNotInTargetTypeException} is thrown when a + * {@link DistributionSetType} is requested as part of a {@link TargetType} but + * is not returned in {@link TargetType#getCompatibleDistributionSetTypes()}. + */ +public class DistributionSetTypeNotInTargetTypeException extends EntityNotFoundException { + + private static final long serialVersionUID = 1L; + + /** + * Constructor + * + * @param distributionSetTypeId + * that is not part of given {@link TargetType} + * @param targetTypeId + * of the {@link TargetType} where given + * {@link DistributionSetType} is not part of + */ + public DistributionSetTypeNotInTargetTypeException(final Long distributionSetTypeId, + final Long targetTypeId) { + super(DistributionSetType.class.getSimpleName() + " with id {" + distributionSetTypeId + "} is not part of " + + TargetType.class.getSimpleName() + " with id {" + targetTypeId + "}."); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TargetTypeInUseException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TargetTypeInUseException.java new file mode 100644 index 000000000..e27ac3735 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/TargetTypeInUseException.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * Thrown if target type is assigned + */ +public class TargetTypeInUseException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_TARGET_TYPE_IN_USE; + + /** + * Default constructor. + */ + public TargetTypeInUseException() { + super(THIS_ERROR); + } + + /** + * Parameterized constructor. + * + * @param cause + * of the exception + */ + public TargetTypeInUseException(final Throwable cause) { + super(THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + * @param cause + * of the exception + */ + public TargetTypeInUseException(final String message, final Throwable cause) { + super(message, THIS_ERROR, cause); + } + + /** + * Parameterized constructor. + * + * @param message + * of the exception + */ + public TargetTypeInUseException(final String message) { + super(message, THIS_ERROR); + } +} + diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java index 952c432e7..bd9353be8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Target.java @@ -85,6 +85,11 @@ public interface Target extends NamedEntity { */ TargetUpdateStatus getUpdateStatus(); + /** + * @return Target type {@link TargetType}. + */ + TargetType getTargetType(); + /** * @return the poll time which holds the last poll time of the target, the * next poll time and the overdue time. In case the diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetType.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetType.java new file mode 100644 index 000000000..a705d2459 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetType.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.util.Set; + +/** + * A {@link TargetType} is an abstract definition for + * {@link Target} + * + */ +public interface TargetType extends NamedEntity { + /** + * Maximum length of color in Management UI. + */ + int COLOUR_MAX_SIZE = 16; + + /** + * @return immutable set of optional {@link DistributionSetType}s + */ + Set getCompatibleDistributionSetTypes(); + + /** + * @return immutable set of optional {@link Target}s + */ + Set getTargets(); + + /** + * Checks if the given {@link DistributionSetType} is in + * {@link #getCompatibleDistributionSetTypes()}. + * + * @param distributionSetType + * search for + * @return true if found + */ + default boolean containsCompatibleDistributionSetType(final DistributionSetType distributionSetType) { + return containsCompatibleDistributionSetType(distributionSetType.getId()); + } + + /** + * Checks if the given {@link DistributionSetType} is in + * {@link #getCompatibleDistributionSetTypes()}. + * + * @param distributionSetTypeId + * search by {@link DistributionSetType#getId()} + * @return true if found + */ + default boolean containsCompatibleDistributionSetType(final Long distributionSetTypeId) { + return getCompatibleDistributionSetTypes().stream().anyMatch(element -> element.getId().equals(distributionSetTypeId)); + } + + /** + * Unassigns a distribution set type from target types + * @param dsTypeId + * @return the resulting target type + */ + TargetType removeDistributionSetType(final Long dsTypeId); + + /** + * @return get color code to be used in management UI views. + */ + String getColour(); +} diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java index ff05dac18..4ab876288 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java @@ -108,4 +108,8 @@ public class PropertiesQuotaManagement implements QuotaManagement { return securityProperties.getDos().getMaxArtifactStorage(); } + @Override + public int getMaxDistributionSetTypesPerTargetType() { + return securityProperties.getDos().getMaxDistributionSetTypesPerTargetType(); + } } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetTypeUpdateCreate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetTypeUpdateCreate.java new file mode 100644 index 000000000..2a30d732f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetTypeUpdateCreate.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.builder; + +import org.eclipse.hawkbit.repository.ValidString; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Optional; + +/** + * Create and update builder DTO. + * + * @param + * update or create builder interface + */ +public abstract class AbstractTargetTypeUpdateCreate extends AbstractNamedEntityBuilder { + @ValidString + protected String colour; + + protected Collection compatible; + + /** + * @param compatible + * list of ID + * @return generic type + */ + public T compatible(final Collection compatible) { + this.compatible = compatible; + return (T) this; + } + + /** + * @return List of ID + */ + public Optional> getCompatible() { + return Optional.ofNullable(compatible); + } + + /** + * @param colour + * Colour value + * @return generic type + */ + public T colour(final String colour) { + this.colour = StringUtils.trimWhitespace(colour); + return (T) this; + } + + /** + * @return colour + */ + public Optional getColour() { + return Optional.ofNullable(colour); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetUpdateCreate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetUpdateCreate.java index 405402339..4ff522bc8 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetUpdateCreate.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractTargetUpdateCreate.java @@ -11,8 +11,11 @@ package org.eclipse.hawkbit.repository.builder; import java.net.URI; import java.util.Optional; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.ValidString; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAddressException; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.springframework.util.StringUtils; @@ -36,6 +39,8 @@ public class AbstractTargetUpdateCreate extends AbstractNamedEntityBuilder protected Boolean requestAttributes; + protected Long targetTypeId; + protected AbstractTargetUpdateCreate(final String controllerId) { this.controllerId = StringUtils.trimWhitespace(controllerId); } @@ -79,6 +84,11 @@ public class AbstractTargetUpdateCreate extends AbstractNamedEntityBuilder return (TargetCreate) this; } + public T targetType(final Long targetTypeId) { + this.targetTypeId = targetTypeId; + return (T) this; + } + public String getControllerId() { return controllerId; } @@ -99,4 +109,8 @@ public class AbstractTargetUpdateCreate extends AbstractNamedEntityBuilder return Optional.ofNullable(status); } + public Long getTargetTypeId() { + return targetTypeId; + } + } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericTargetTypeUpdate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericTargetTypeUpdate.java new file mode 100644 index 000000000..b60d0e41f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/GenericTargetTypeUpdate.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.builder; + +/** + * Update implementation. + */ +public class GenericTargetTypeUpdate extends AbstractTargetTypeUpdateCreate + implements TargetTypeUpdate { + + /** + * @param id + * Target type ID + */ + public GenericTargetTypeUpdate(final Long id) { + super.id = id; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java index a7675bf93..755c933aa 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetTypeCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetTypeSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; @@ -66,6 +67,8 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana private final DistributionSetRepository distributionSetRepository; + private final TargetTypeRepository targetTypeRepository; + private final VirtualPropertyReplacer virtualPropertyReplacer; private final Database database; @@ -74,12 +77,13 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana JpaDistributionSetTypeManagement(final DistributionSetTypeRepository distributionSetTypeRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, - final DistributionSetRepository distributionSetRepository, + final DistributionSetRepository distributionSetRepository, final TargetTypeRepository targetTypeRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, final QuotaManagement quotaManagement) { this.distributionSetTypeRepository = distributionSetTypeRepository; this.softwareModuleTypeRepository = softwareModuleTypeRepository; this.distributionSetRepository = distributionSetRepository; + this.targetTypeRepository = targetTypeRepository; this.virtualPropertyReplacer = virtualPropertyReplacer; this.database = database; this.quotaManagement = quotaManagement; @@ -190,7 +194,7 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana } /** - * Enforces the quota specifiying the maximum number of + * Enforces the quota specifying the maximum number of * {@link SoftwareModuleType}s per {@link DistributionSetType}. * * @param id @@ -268,18 +272,28 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public void delete(final long typeId) { - final JpaDistributionSetType toDelete = distributionSetTypeRepository.findById(typeId) .orElseThrow(() -> new EntityNotFoundException(DistributionSetType.class, typeId)); + unassignDsTypeFromTargetTypes(typeId); + if (distributionSetRepository.countByTypeId(typeId) > 0) { toDelete.setDeleted(true); distributionSetTypeRepository.save(toDelete); - } else { + } + else { distributionSetTypeRepository.deleteById(typeId); } } + private void unassignDsTypeFromTargetTypes(long typeId) { + List targetTypesByDsType = targetTypeRepository.findByDsType(typeId); + targetTypesByDsType.forEach(targetType -> { + targetType.removeDistributionSetType(typeId); + targetTypeRepository.save(targetType); + }); + } + @Override @Transactional @Retryable(include = { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java index b620571e8..ac1644708 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaEntityFactory.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.repository.builder.SoftwareModuleTypeBuilder; import org.eclipse.hawkbit.repository.builder.TagBuilder; import org.eclipse.hawkbit.repository.builder.TargetBuilder; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryBuilder; +import org.eclipse.hawkbit.repository.builder.TargetTypeBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleTypeBuilder; @@ -42,6 +43,9 @@ public class JpaEntityFactory implements EntityFactory { @Autowired private DistributionSetBuilder distributionSetBuilder; + @Autowired + private TargetBuilder targetBuilder; + @Autowired private DistributionSetTypeBuilder distributionSetTypeBuilder; @@ -57,6 +61,9 @@ public class JpaEntityFactory implements EntityFactory { @Autowired private SoftwareModuleMetadataBuilder softwareModuleMetadataBuilder; + @Autowired + private TargetTypeBuilder targetTypeBuilder; + @Override public MetaData generateDsMetadata(final String key, final String value) { return new JpaDistributionSetMetadata(key, StringUtils.trimWhitespace(value)); @@ -79,7 +86,12 @@ public class JpaEntityFactory implements EntityFactory { @Override public TargetBuilder target() { - return new JpaTargetBuilder(); + return targetBuilder; + } + + @Override + public TargetTypeBuilder targetType() { + return targetTypeBuilder; } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java index 859832b78..74c6855c7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java @@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; import org.eclipse.hawkbit.repository.jpa.model.JpaTenantMetaData; import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.model.DistributionSetType; @@ -93,6 +94,9 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst @Autowired private TargetTagRepository targetTagRepository; + @Autowired + private TargetTypeRepository targetTypeRepository; + @Autowired private DistributionSetTagRepository distributionSetTagRepository; @@ -255,6 +259,7 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst targetRepository.deleteByTenant(tenant); targetFilterQueryRepository.deleteByTenant(tenant); rolloutRepository.deleteByTenant(tenant); + targetTypeRepository.deleteByTenant(tenant); targetTagRepository.deleteByTenant(tenant); distributionSetTagRepository.deleteByTenant(tenant); distributionSetRepository.deleteByTenant(tenant); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index 178cf9d72..e9176fb30 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -49,6 +49,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetMetadata_; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.model.TargetMetadataCompositeKey; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; @@ -63,6 +64,7 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; @@ -98,6 +100,8 @@ public class JpaTargetManagement implements TargetManagement { private final TargetRepository targetRepository; + private final TargetTypeRepository targetTypeRepository; + private final TargetMetadataRepository targetMetadataRepository; private final RolloutGroupRepository rolloutGroupRepository; @@ -119,7 +123,7 @@ public class JpaTargetManagement implements TargetManagement { private final Database database; public JpaTargetManagement(final EntityManager entityManager, final QuotaManagement quotaManagement, - final TargetRepository targetRepository, final TargetMetadataRepository targetMetadataRepository, + final TargetRepository targetRepository, final TargetTypeRepository targetTypeRepository, final TargetMetadataRepository targetMetadataRepository, final RolloutGroupRepository rolloutGroupRepository, final DistributionSetRepository distributionSetRepository, final TargetFilterQueryRepository targetFilterQueryRepository, @@ -130,6 +134,7 @@ public class JpaTargetManagement implements TargetManagement { this.entityManager = entityManager; this.quotaManagement = quotaManagement; this.targetRepository = targetRepository; + this.targetTypeRepository = targetTypeRepository; this.targetMetadataRepository = targetMetadataRepository; this.rolloutGroupRepository = rolloutGroupRepository; this.distributionSetRepository = distributionSetRepository; @@ -152,6 +157,10 @@ public class JpaTargetManagement implements TargetManagement { .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); } + private JpaTargetType getTargetTypeByIdAndThrowIfNotFound(final long id) { + return targetTypeRepository.findById(id).orElseThrow(() -> new EntityNotFoundException(TargetType.class, id)); + } + @Override public List getByControllerID(final Collection controllerIDs) { return Collections.unmodifiableList( @@ -333,6 +342,10 @@ public class JpaTargetManagement implements TargetManagement { update.getDescription().ifPresent(target::setDescription); update.getAddress().ifPresent(target::setAddress); update.getSecurityToken().ifPresent(target::setSecurityToken); + if (update.getTargetTypeId() != null){ + TargetType targetType = getTargetTypeByIdAndThrowIfNotFound(update.getTargetTypeId()); + target.setTargetType(targetType); + } return targetRepository.save(target); } @@ -589,6 +602,24 @@ public class JpaTargetManagement implements TargetManagement { return result; } + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public Target unAssignType(final String controllerID) { + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerID); + target.setTargetType(null); + return targetRepository.save(target); + } + + @Override + public Target assignType(String controllerID, long targetTypeId) { + final JpaTarget target = getByControllerIdAndThrowIfNotFound(controllerID); + final JpaTargetType targetType = getTargetTypeByIdAndThrowIfNotFound(targetTypeId); + target.setTargetType(targetType); + return targetRepository.save(target); + } + @Override public Slice findByFilterOrderByLinkedDistributionSet(final Pageable pageable, final long orderByDistributionId, final FilterParams filterParams) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTypeManagement.java new file mode 100644 index 000000000..34a9a0997 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTypeManagement.java @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import org.eclipse.hawkbit.repository.QuotaManagement; +import org.eclipse.hawkbit.repository.TargetTypeFields; +import org.eclipse.hawkbit.repository.TargetTypeManagement; +import org.eclipse.hawkbit.repository.builder.GenericTargetTypeUpdate; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate; +import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.TargetTypeInUseException; +import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetTypeCreate; +import org.eclipse.hawkbit.repository.jpa.configuration.Constants; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * JPA implementation of {@link TargetTypeManagement}. + * + */ +@Transactional(readOnly = true) +@Validated +public class JpaTargetTypeManagement implements TargetTypeManagement { + + private final TargetTypeRepository targetTypeRepository; + private final TargetRepository targetRepository; + private final DistributionSetTypeRepository distributionSetTypeRepository; + + private final VirtualPropertyReplacer virtualPropertyReplacer; + + private final Database database; + private final QuotaManagement quotaManagement; + + /** + * Constructor + * + * @param targetTypeRepository + * Target type repository + * @param targetRepository + * Target repository + * @param virtualPropertyReplacer + * replacer + * @param database + * database + */ + public JpaTargetTypeManagement(final TargetTypeRepository targetTypeRepository, + final TargetRepository targetRepository, final DistributionSetTypeRepository distributionSetTypeRepository, + final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, + final QuotaManagement quotaManagement) { + this.targetTypeRepository = targetTypeRepository; + this.targetRepository = targetRepository; + this.distributionSetTypeRepository = distributionSetTypeRepository; + this.virtualPropertyReplacer = virtualPropertyReplacer; + this.database = database; + this.quotaManagement = quotaManagement; + } + + @Override + public Optional getByName(String name) { + return targetTypeRepository.findByName(name).map(TargetType.class::cast); + } + + @Override + public long count() { + return targetTypeRepository.count(); + } + + @Override + public TargetType create(TargetTypeCreate create) { + final JpaTargetTypeCreate typeCreate = (JpaTargetTypeCreate) create; + return targetTypeRepository.save(typeCreate.build()); + } + + @Override + public List create(Collection creates) { + return creates.stream().map(this::create).collect(Collectors.toList()); + } + + @Override + @Transactional + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public void delete(final Long targetTypeId) { + throwExceptionIfTargetTypeDoesNotExist(targetTypeId); + + if (targetRepository.countByTargetTypeId(targetTypeId) > 0) { + throw new TargetTypeInUseException("Cannot delete target type that is in use"); + } + + targetTypeRepository.deleteById(targetTypeId); + } + + @Override + public Page findAll(Pageable pageable) { + return convertPage(targetTypeRepository.findAll(pageable), pageable); + } + + @Override + public Page findByRsql(Pageable pageable, String rsqlParam) { + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetTypeFields.class, + virtualPropertyReplacer, database); + + return convertPage(targetTypeRepository.findAll(spec, pageable), pageable); + } + + @Override + public Optional get(long id) { + return targetTypeRepository.findById(id).map(targetType -> targetType); + } + + @Override + public Optional findByTargetId(long targetId) { + return targetTypeRepository + .findOne(TargetTypeSpecification.hasTarget(targetId)).map(TargetType.class::cast); + } + + @Override + public List findByTargetIds(Collection targetIds) { + return targetTypeRepository.findAll(TargetTypeSpecification.hasTarget(targetIds)) + .stream().map(TargetType.class::cast).collect(Collectors.toList()); + } + + @Override + public Optional findByTargetControllerId(String controllerId) { + return targetTypeRepository + .findOne(TargetTypeSpecification.hasTargetControllerId(controllerId)).map(TargetType.class::cast); + } + + @Override + public List findByTargetControllerIds(Collection controllerIds) { + return targetTypeRepository.findAll(TargetTypeSpecification.hasTargetControllerIdIn(controllerIds)) + .stream().map(TargetType.class::cast).collect(Collectors.toList()); + } + + @Override + public List get(Collection ids) { + return Collections.unmodifiableList(targetTypeRepository.findAllById(ids)); + } + + @Override + public TargetType update(TargetTypeUpdate update) { + final GenericTargetTypeUpdate typeUpdate = (GenericTargetTypeUpdate) update; + + final JpaTargetType type = findTargetTypeAndThrowExceptionIfNotFound(typeUpdate.getId()); + + typeUpdate.getName().ifPresent((type::setName)); + typeUpdate.getDescription().ifPresent(type::setDescription); + typeUpdate.getColour().ifPresent(type::setColour); + + return targetTypeRepository.save(type); + } + + @Override + public TargetType assignCompatibleDistributionSetTypes(long targetTypeId, Collection distributionSetTypeIds) { + final Collection dsTypes = distributionSetTypeRepository + .findAllById(distributionSetTypeIds); + + if (dsTypes.size() < distributionSetTypeIds.size()) { + throw new EntityNotFoundException(DistributionSetType.class, distributionSetTypeIds, + dsTypes.stream().map(DistributionSetType::getId).collect(Collectors.toList())); + } + + final JpaTargetType type = findTargetTypeAndThrowExceptionIfNotFound(targetTypeId); + assertDistributionSetTypeQuota(targetTypeId, distributionSetTypeIds.size()); + + dsTypes.forEach(type::addCompatibleDistributionSetType); + + return targetTypeRepository.save(type); + } + + @Override + public TargetType unassignDistributionSetType(long targetTypeId, long distributionSetTypeId) { + final JpaTargetType type = findTargetTypeAndThrowExceptionIfNotFound(targetTypeId); + findDsTypeAndThrowExceptionIfNotFound(distributionSetTypeId); + type.removeDistributionSetType(distributionSetTypeId); + + return targetTypeRepository.save(type); + } + + private JpaTargetType findTargetTypeAndThrowExceptionIfNotFound(final Long typeId) { + return (JpaTargetType) get(typeId).orElseThrow(() -> new EntityNotFoundException(TargetType.class, typeId)); + } + + private void findDsTypeAndThrowExceptionIfNotFound(final Long typeId) { + distributionSetTypeRepository.findById(typeId).orElseThrow(() -> new EntityNotFoundException(DistributionSetType.class, typeId)); + } + + private void throwExceptionIfTargetTypeDoesNotExist(final Long typeId) { + if (!targetTypeRepository.existsById(typeId)) { + throw new EntityNotFoundException(TargetType.class, typeId); + } + } + + /** + * Enforces the quota specifying the maximum number of + * {@link DistributionSetType}s per {@link TargetType}. + * + * @param id + * of the target type + * @param requested + * number of distribution set types to check + * + * @throws AssignmentQuotaExceededException + * if the software module type quota is exceeded + */ + private void assertDistributionSetTypeQuota(final long id, final int requested) { + QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxDistributionSetTypesPerTargetType(), + DistributionSetType.class, TargetType.class, targetTypeRepository::countDsSetTypesById); + } + + private static Page convertPage(final Page findAll, final Pageable pageable) { + return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements()); + } + + private static Slice convertPage(final Slice findAll, final Pageable pageable) { + return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, 0); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index db37513bb..b9d8e66e4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -38,6 +38,7 @@ import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.TenantStatsManagement; import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor; @@ -46,7 +47,9 @@ import org.eclipse.hawkbit.repository.builder.DistributionSetTypeBuilder; import org.eclipse.hawkbit.repository.builder.RolloutBuilder; import org.eclipse.hawkbit.repository.builder.SoftwareModuleBuilder; import org.eclipse.hawkbit.repository.builder.SoftwareModuleMetadataBuilder; +import org.eclipse.hawkbit.repository.builder.TargetBuilder; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryBuilder; +import org.eclipse.hawkbit.repository.builder.TargetTypeBuilder; import org.eclipse.hawkbit.repository.event.ApplicationEventFilter; import org.eclipse.hawkbit.repository.event.remote.EventEntityManager; import org.eclipse.hawkbit.repository.event.remote.EventEntityManagerHolder; @@ -62,7 +65,9 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetTypeBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleMetadataBuilder; +import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder; +import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetTypeBuilder; import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager; import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitDefaultServiceExecutor; @@ -85,6 +90,7 @@ import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; @@ -231,6 +237,21 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { return new JpaDistributionSetBuilder(distributionSetTypeManagement, softwareManagement); } + @Bean + TargetBuilder targetBuilder(final TargetTypeManagement targetTypeManagement){ + return new JpaTargetBuilder(targetTypeManagement); + } + + /** + * @param dsTypeManagement + * for loading {@link TargetType#getCompatibleDistributionSetTypes()} + * @return TargetTypeBuilder bean + */ + @Bean + TargetTypeBuilder targetTypeBuilder(final DistributionSetTypeManagement dsTypeManagement) { + return new JpaTargetTypeBuilder(dsTypeManagement); + } + @Bean SoftwareModuleMetadataBuilder softwareModuleMetadataBuilder( final SoftwareModuleManagement softwareModuleManagement) { @@ -465,10 +486,26 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final DistributionSetTypeRepository distributionSetTypeRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, final DistributionSetRepository distributionSetRepository, + final TargetTypeRepository targetTypeRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties, final QuotaManagement quotaManagement) { return new JpaDistributionSetTypeManagement(distributionSetTypeRepository, softwareModuleTypeRepository, - distributionSetRepository, virtualPropertyReplacer, properties.getDatabase(), quotaManagement); + distributionSetRepository, targetTypeRepository, virtualPropertyReplacer, properties.getDatabase(), quotaManagement); + } + + /** + * {@link JpaTargetTypeManagement} bean. + * + * @return a new {@link TargetTypeManagement} + */ + @Bean + @ConditionalOnMissingBean + TargetTypeManagement targetTypeManagement(final TargetTypeRepository targetTypeRepository, + final TargetRepository targetRepository, final DistributionSetTypeRepository distributionSetTypeRepository, + final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties, + final QuotaManagement quotaManagement) { + return new JpaTargetTypeManagement(targetTypeRepository, targetRepository, distributionSetTypeRepository, + virtualPropertyReplacer, properties.getDatabase(), quotaManagement); } /** @@ -504,11 +541,11 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final TargetRepository targetRepository, final TargetMetadataRepository targetMetadataRepository, final RolloutGroupRepository rolloutGroupRepository, final DistributionSetRepository distributionSetRepository, - final TargetFilterQueryRepository targetFilterQueryRepository, + final TargetFilterQueryRepository targetFilterQueryRepository, final TargetTypeRepository targetTypeRepository, final TargetTagRepository targetTagRepository, final EventPublisherHolder eventPublisherHolder, final TenantAware tenantAware, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties) { - return new JpaTargetManagement(entityManager, quotaManagement, targetRepository, targetMetadataRepository, + return new JpaTargetManagement(entityManager, quotaManagement, targetRepository, targetTypeRepository, targetMetadataRepository, rolloutGroupRepository, distributionSetRepository, targetFilterQueryRepository, targetTagRepository, eventPublisherHolder, tenantAware, afterCommit, virtualPropertyReplacer, properties.getDatabase()); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java index 0746d509f..59d7501a5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetRepository.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.springframework.data.domain.Page; @@ -200,6 +201,15 @@ public interface TargetRepository extends BaseEntityRepository, return this.count(Specification.where(null)); } + /** + * Counts {@link Target} instances of given type in the repository. + * + * @param targetTypeId + * to search for + * @return number of found {@link Target}s + */ + long countByTargetTypeId(Long targetTypeId); + /** * Deletes all {@link TenantAwareBaseEntity} of a given tenant. For safety * reasons (this is a "delete everything" query after all) we add the tenant diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java new file mode 100644 index 000000000..c23936155 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.lang.NonNull; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * {@link PagingAndSortingRepository} for {@link JpaTargetType}. + * + */ +@Transactional(readOnly = true) +public interface TargetTypeRepository + extends BaseEntityRepository, JpaSpecificationExecutor { + + /** + * Finds {@link TargetType} in the repository matching an id. + * + * Calls version based on spec to allow injecting further specs + * + * @param id id to filter for + * @return {@link Optional} of {@link TargetType} + */ + @Override + @NonNull + default Optional findById(@NonNull Long id) { + return this.findOne(Specification.where(TargetTypeSpecification.hasId(id))); + } + + /** + * @param ids + * List of ID + * @return Target type list + */ + @Override + @NonNull + default List findAllById(Iterable ids) { + final List collectedIds = StreamSupport.stream(ids.spliterator(), true).collect(Collectors.toList()); + return this.findAll(Specification.where(TargetTypeSpecification.hasIdIn(collectedIds))); + } + + /** + * Deletes the {@link TargetType}s with the given target IDs. + * + * @param targetTypeIDs + * to be deleted + */ + @Modifying + @Transactional + @Query("DELETE FROM JpaTargetType t WHERE t.id IN ?1") + void deleteByIdIn(Collection targetTypeIDs); + + /** + * Finds all {@link TargetType}s in the repository. + * + * Calls version with (empty) spec to allow injecting further specs + * + * @return {@link List} of {@link TargetType}s + */ + @Override + @NonNull + default List findAll() { + return this.findAll(Specification.where(null)); + } + + /** + * Finds all {@link TargetType}s in the repository sorted. + * + * Calls version with (empty) spec to allow injecting further specs + * + * @param sort instructions to sort result by + * @return {@link List} of {@link TargetType}s + */ + @Override + @NonNull + default Iterable findAll(@NonNull Sort sort) { + return this.findAll(Specification.where(null), sort); + } + + /** + * Finds a page of {@link TargetType}s in the repository. + * + * Calls version with (empty) spec to allow injecting further specs + * + * @param pageable paging context + * @return {@link List} of {@link TargetType}s + */ + @Override + @NonNull + default Page findAll(@NonNull Pageable pageable) { + return this.findAll(Specification.where(null), pageable); + } + + /** + * Checks whether {@link TargetType} in the repository matching an id exists or not. + * + * Calls version based on spec to allow injecting further specs + * + * @param id id to check for + * @return true if TargetType with id exists + */ + @Override + default boolean existsById(@NonNull Long id) { + return this.exists(TargetTypeSpecification.hasId(id)); + } + + /** + * Checks whether {@link TargetType} in the repository matching a spec exists or not. + * + * @param spec to check for existence + * @return true if target with id exists + */ + default boolean exists(@NonNull Specification spec) { + return this.count(spec) > 0; + } + + /** + * Count number of {@link TargetType}s in the repository. + * + * Calls version with an empty spec to allow injecting further specs + * + * @return number of targetTypes in the repository + */ + @Override + default long count() { + return this.count(Specification.where(null)); + } + + /** + * @param tenant + * Tenant + */ + @Modifying + @Transactional + @Query("DELETE FROM JpaTargetType t WHERE t.tenant = :tenant") + void deleteByTenant(@Param("tenant") String tenant); + + @Query(value = "SELECT COUNT (t.id) FROM JpaDistributionSetType t JOIN t.compatibleToTargetTypes tt WHERE tt.id = :id") + long countDsSetTypesById(@Param("id") Long id); + + /** + * + * @param dsTypeId + * to search for + * @return all {@link TargetType}s in the repository with given + * {@link TargetType#getName()} + */ + default List findByDsType(@Param("id") Long dsTypeId) { + return this.findAll(Specification.where(TargetTypeSpecification.hasDsSetType(dsTypeId))); + } + + /** + * + * @param name + * to search for + * @return all {@link TargetType}s in the repository with given + * {@link TargetType#getName()} + */ + default Optional findByName(String name){ + return this.findOne(Specification.where(TargetTypeSpecification.hasName(name))); + }; +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetBuilder.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetBuilder.java index b9c07b7db..48f7109e2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetBuilder.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetBuilder.java @@ -8,15 +8,26 @@ */ package org.eclipse.hawkbit.repository.jpa.builder; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.builder.TargetBuilder; import org.eclipse.hawkbit.repository.builder.TargetCreate; import org.eclipse.hawkbit.repository.builder.TargetUpdate; +import org.eclipse.hawkbit.repository.model.Target; /** * Builder implementation for {@link Target}. * */ public class JpaTargetBuilder implements TargetBuilder { + final private TargetTypeManagement targetTypeManagement; + + /** + * @param targetTypeManagement + * Target type management + */ + public JpaTargetBuilder(TargetTypeManagement targetTypeManagement) { + this.targetTypeManagement = targetTypeManagement; + } @Override public TargetUpdate update(final String controllerId) { @@ -25,7 +36,7 @@ public class JpaTargetBuilder implements TargetBuilder { @Override public TargetCreate create() { - return new JpaTargetCreate(); + return new JpaTargetCreate(targetTypeManagement); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetCreate.java index b76394985..d1701431a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetCreate.java @@ -8,9 +8,12 @@ */ package org.eclipse.hawkbit.repository.jpa.builder; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.builder.AbstractTargetUpdateCreate; import org.eclipse.hawkbit.repository.builder.TargetCreate; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.springframework.util.StringUtils; @@ -20,8 +23,17 @@ import org.springframework.util.StringUtils; */ public class JpaTargetCreate extends AbstractTargetUpdateCreate implements TargetCreate { - JpaTargetCreate() { + private final TargetTypeManagement targetTypeManagement; + + /** + * Constructor + * + * @param targetTypeManagement + * Target type management + */ + JpaTargetCreate(final TargetTypeManagement targetTypeManagement) { super(null); + this.targetTypeManagement = targetTypeManagement; } @Override @@ -38,6 +50,12 @@ public class JpaTargetCreate extends AbstractTargetUpdateCreate im target.setName(name); } + if (targetTypeId != null){ + TargetType targetType = targetTypeManagement.get(targetTypeId) + .orElseThrow(() -> new EntityNotFoundException(TargetType.class, targetTypeId)); + target.setTargetType(targetType); + } + target.setDescription(description); target.setAddress(address); target.setUpdateStatus(getStatus().orElse(TargetUpdateStatus.UNKNOWN)); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeBuilder.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeBuilder.java new file mode 100644 index 000000000..2ab280248 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeBuilder.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.builder; + +import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.TargetTypeManagement; +import org.eclipse.hawkbit.repository.builder.GenericTargetTypeUpdate; +import org.eclipse.hawkbit.repository.builder.TargetTypeBuilder; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate; +import org.eclipse.hawkbit.repository.model.TargetType; + +/** + * Builder implementation for {@link TargetType}. + * + */ +public class JpaTargetTypeBuilder implements TargetTypeBuilder { + private final DistributionSetTypeManagement distributionSetTypeManagement; + + /** + * Constructor + * + * @param distributionSetTypeManagement + * Distribution set type management + */ + public JpaTargetTypeBuilder(DistributionSetTypeManagement distributionSetTypeManagement) { + this.distributionSetTypeManagement = distributionSetTypeManagement; + } + + @Override + public TargetTypeUpdate update(long id) { + return new GenericTargetTypeUpdate(id); + } + + @Override + public TargetTypeCreate create() { + return new JpaTargetTypeCreate(distributionSetTypeManagement); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeCreate.java new file mode 100644 index 000000000..67a0d7b55 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaTargetTypeCreate.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.builder; + +import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.builder.AbstractTargetTypeUpdateCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.Collections; + +/** + * Create/build implementation. + * + */ +public class JpaTargetTypeCreate extends AbstractTargetTypeUpdateCreate + implements TargetTypeCreate { + + private final DistributionSetTypeManagement distributionSetTypeManagement; + + /** + * Constructor + * + * @param distributionSetTypeManagement + * Distribution set type management + */ + JpaTargetTypeCreate(final DistributionSetTypeManagement distributionSetTypeManagement) { + this.distributionSetTypeManagement = distributionSetTypeManagement; + } + + @Override + public JpaTargetType build() { + final JpaTargetType result = new JpaTargetType(name, description, colour); + + findDistributionSetTypeWithExceptionIfNotFound(compatible).forEach(result::addCompatibleDistributionSetType); + + return result; + } + + private Collection findDistributionSetTypeWithExceptionIfNotFound( + final Collection distributionSetTypeId) { + if (CollectionUtils.isEmpty(distributionSetTypeId)) { + return Collections.emptyList(); + } + + final Collection type = distributionSetTypeManagement.get(distributionSetTypeId); + if (type.size() < distributionSetTypeId.size()) { + throw new EntityNotFoundException(SoftwareModuleType.class, distributionSetTypeId); + } + + return type; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java index c81f66b44..b55ac92a5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSetType.java @@ -8,24 +8,6 @@ */ package org.eclipse.hawkbit.repository.jpa.model; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Index; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - import org.eclipse.hawkbit.repository.event.remote.DistributionSetTypeDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTypeCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTypeUpdatedEvent; @@ -33,11 +15,30 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.persistence.annotations.CascadeOnDelete; import org.eclipse.persistence.descriptors.DescriptorEvent; import org.springframework.util.CollectionUtils; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Index; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + /** * A distribution set type defines which software module types can or have to be * {@link DistributionSet}. @@ -72,6 +73,10 @@ public class JpaDistributionSetType extends AbstractJpaNamedEntity implements Di @Column(name = "deleted") private boolean deleted; + @CascadeOnDelete + @ManyToMany(mappedBy = "distributionSetTypes", targetEntity = JpaTargetType.class, fetch = FetchType.LAZY) + private List compatibleToTargetTypes; + public JpaDistributionSetType() { // default public constructor for JPA } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java index 92ecb7c40..a3181d05e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java @@ -55,6 +55,7 @@ import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; @@ -162,6 +163,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw @JoinColumn(name = "target_id", nullable = false, updatable = false) }, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_targ_attrib_target")) private Map controllerAttributes; + @ManyToOne(fetch = FetchType.LAZY, optional = true, targetEntity = JpaTargetType.class) + @JoinColumn(name = "target_type", nullable = true, updatable = true, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_relation_target_type")) + private TargetType targetType; + // set default request controller attributes to true, because we want to // request them the first // time @@ -201,10 +206,16 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw : controllerId; } - JpaTarget() { + /** + * Constructor + */ + public JpaTarget() { // empty constructor for JPA. } + /** + * @return assigned distribution set + */ public DistributionSet getAssignedDistributionSet() { return assignedDistributionSet; } @@ -214,6 +225,9 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return controllerId; } + /** + * @return tags + */ public Set getTags() { if (tags == null) { return Collections.emptySet(); @@ -222,6 +236,9 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return Collections.unmodifiableSet(tags); } + /** + * @return rollouts target group + */ public List getRolloutTargetGroup() { if (rolloutTargetGroup == null) { return Collections.emptyList(); @@ -230,6 +247,11 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return Collections.unmodifiableList(rolloutTargetGroup); } + /** + * @param tag + * tag + * @return boolean true or false + */ public boolean addTag(final TargetTag tag) { if (tags == null) { tags = new HashSet<>(); @@ -238,6 +260,11 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return tags.add(tag); } + /** + * @param tag + * tag + * @return boolean true or false + */ public boolean removeTag(final TargetTag tag) { if (tags == null) { return false; @@ -246,14 +273,25 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return tags.remove(tag); } + /** + * @param assignedDistributionSet + * Distribution set + */ public void setAssignedDistributionSet(final DistributionSet assignedDistributionSet) { this.assignedDistributionSet = (JpaDistributionSet) assignedDistributionSet; } + /** + * @param controllerId + * Controller ID + */ public void setControllerId(final String controllerId) { this.controllerId = controllerId; } + /** + * @return list of action + */ public List getActions() { if (actions == null) { return Collections.emptyList(); @@ -262,6 +300,11 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return Collections.unmodifiableList(actions); } + /** + * @param action + * Action + * @return boolean true or false + */ public boolean addAction(final Action action) { if (actions == null) { actions = new ArrayList<>(); @@ -285,6 +328,10 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return null; } + /** + * @param securityToken + * token value + */ public void setSecurityToken(final String securityToken) { this.securityToken = securityToken; } @@ -348,10 +395,29 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return updateStatus; } + @Override + public TargetType getTargetType() { + return targetType; + } + + /** + * @param type + * Target type + */ + public void setTargetType(final TargetType type) { + this.targetType = type; + } + + /** + * @return Distribution set + */ public JpaDistributionSet getInstalledDistributionSet() { return installedDistributionSet; } + /** + * @return controller attributes + */ public Map getControllerAttributes() { return controllerAttributes; } @@ -361,6 +427,9 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw return requestControllerAttributes; } + /** + * @return target metadata + */ public List getMetadata() { if (metadata == null) { return Collections.emptyList(); @@ -375,26 +444,50 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw + "]"; } + /** + * @param address + * Address + */ public void setAddress(final String address) { this.address = address; } + /** + * @param lastTargetQuery + * last query ID + */ public void setLastTargetQuery(final Long lastTargetQuery) { this.lastTargetQuery = lastTargetQuery; } + /** + * @param installationDate + * installation date + */ public void setInstallationDate(final Long installationDate) { this.installationDate = installationDate; } + /** + * @param installedDistributionSet + * Distribution set + */ public void setInstalledDistributionSet(final JpaDistributionSet installedDistributionSet) { this.installedDistributionSet = installedDistributionSet; } + /** + * @param updateStatus + * Status + */ public void setUpdateStatus(final TargetUpdateStatus updateStatus) { this.updateStatus = updateStatus; } + /** + * @param requestControllerAttributes + * Attributes + */ public void setRequestControllerAttributes(final boolean requestControllerAttributes) { this.requestControllerAttributes = requestControllerAttributes; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetType.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetType.java new file mode 100644 index 000000000..cbfe0e725 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTargetType.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.model; + +import org.eclipse.hawkbit.repository.event.remote.TargetTypeDeletedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeUpdatedEvent; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; +import org.eclipse.persistence.annotations.CascadeOnDelete; +import org.eclipse.persistence.descriptors.DescriptorEvent; + +import javax.persistence.Column; +import javax.persistence.ConstraintMode; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.Size; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * A target type defines which distribution set types can or have to be + * {@link Target}. + * + */ +@Entity +@Table(name = "sp_target_type", indexes = { + @Index(name = "sp_idx_target_type_prim", columnList = "tenant,id") }, uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "tenant" }, name = "uk_target_type_name")}) +public class JpaTargetType extends AbstractJpaNamedEntity implements TargetType, EventAwareEntity{ + + private static final long serialVersionUID = 1L; + + @Column(name = "colour", nullable = true, length = TargetType.COLOUR_MAX_SIZE) + @Size(max = TargetType.COLOUR_MAX_SIZE) + private String colour; + + @CascadeOnDelete + @ManyToMany(targetEntity = JpaDistributionSetType.class) + @JoinTable(name = "sp_target_type_ds_type_relation", joinColumns = { + @JoinColumn(name = "target_type", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_type_relation_target_type"))}, inverseJoinColumns = { + @JoinColumn(name = "distribution_set_type", nullable = false, updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_type_relation_ds_type"))}) + private Set distributionSetTypes; + + @OneToMany(targetEntity = JpaTarget.class, mappedBy = "targetType", fetch = FetchType.LAZY) + private Set targets; + + /** + * Constructor + */ + public JpaTargetType() { + // default public constructor for JPA + } + + /** + * Constructor + * + * @param name + * Type name + * @param description + * Description + * @param colour + * Colour + */ + public JpaTargetType(String name, String description, String colour) { + super(name,description); + this.colour = colour; + } + + /** + * @param dsSetType + * Distribution set type + * @return Target type + */ + public JpaTargetType addCompatibleDistributionSetType(final DistributionSetType dsSetType) { + if (distributionSetTypes == null) { + distributionSetTypes = new HashSet<>(); + } + + distributionSetTypes.add(dsSetType); + return this; + } + + /** + * @param dsTypeId + * Distribution set type ID + * @return Target type + */ + public JpaTargetType removeDistributionSetType(final Long dsTypeId) { + if (distributionSetTypes == null) { + return this; + } + distributionSetTypes.stream().filter(element -> element.getId().equals(dsTypeId)).findAny() + .ifPresent(distributionSetTypes::remove); + return this; + } + + @Override + public Set getCompatibleDistributionSetTypes() { + + if (distributionSetTypes == null) { + return Collections.emptySet(); + } + + return Collections.unmodifiableSet(distributionSetTypes); + } + + @Override + public Set getTargets() { + return targets; + } + + @Override + public String getColour() { + return colour; + } + + public void setColour(final String colour) { + this.colour = colour; + } + + @Override + public void fireCreateEvent(DescriptorEvent descriptorEvent) { + EventPublisherHolder.getInstance().getEventPublisher().publishEvent( + new TargetTypeCreatedEvent(this, EventPublisherHolder.getInstance().getApplicationId())); + } + + @Override + public void fireUpdateEvent(DescriptorEvent descriptorEvent) { + EventPublisherHolder.getInstance().getEventPublisher().publishEvent( + new TargetTypeUpdatedEvent(this, EventPublisherHolder.getInstance().getApplicationId())); + } + + @Override + public void fireDeleteEvent(DescriptorEvent descriptorEvent) { + EventPublisherHolder.getInstance().getEventPublisher().publishEvent(new TargetTypeDeletedEvent( + getTenant(), getId(), getClass().getName(), EventPublisherHolder.getInstance().getApplicationId())); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetTypeSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetTypeSpecification.java new file mode 100644 index 000000000..734e7e986 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetTypeSpecification.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.specifications; + +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType_; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType_; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.SetJoin; +import java.util.Collection; + +/** + * Specifications class for {@link TargetType}s. The class provides Spring Data JPQL + * Specifications. + * + */ +public final class TargetTypeSpecification { + + private TargetTypeSpecification() { + // utility class + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param id + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasId(final Long id) { + return (targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTargetType_.id), id); + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param id + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasTarget(final long id) { + return (targetRoot, query, cb) -> { + final SetJoin join = targetRoot.join(JpaTargetType_.targets); + return cb.equal(join.get(JpaTarget_.id), id); + }; + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param ids + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasTarget(final Collection ids) { + return (targetRoot, query, cb) -> { + final SetJoin join = targetRoot.join(JpaTargetType_.targets); + return join.get(JpaTarget_.id).in(ids); + }; + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param controllerId + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasTargetControllerId(final String controllerId) { + return (targetRoot, query, cb) -> { + final SetJoin join = targetRoot.join(JpaTargetType_.targets); + return cb.equal(join.get(JpaTarget_.controllerId), controllerId); + }; + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param controllerIds + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasTargetControllerIdIn(final Collection controllerIds) { + return (targetRoot, query, cb) -> { + final SetJoin join = targetRoot.join(JpaTargetType_.targets); + return join.get(JpaTarget_.controllerId).in(controllerIds); + }; + } + + /** + * {@link Specification} for retrieving {@link TargetType}s by controllerId + * + * @param ids + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasIdIn(final Collection ids) { + return (targetRoot, query, cb) -> targetRoot.get(JpaTargetType_.id).in(ids); + } + + /** + * {@link Specification} for retrieving {@link TargetType}s based on a + * {@link DistributionSetType} name. + * + * @param dsTypeId + * to search for + * + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasDsSetType(final Long dsTypeId) { + return (targetRoot, query, cb) -> { + final SetJoin join = targetRoot.join(JpaTargetType_.distributionSetTypes); + return cb.equal(join.get(JpaDistributionSetType_.id), dsTypeId); + }; + } + + + /** + * {@link Specification} for retrieving {@link TargetType} with + * given {@link TargetType#getName()} including fetching the + * elements list. + * + * @param name + * to search + * @return the {@link TargetType} {@link Specification} + */ + public static Specification hasName(final String name) { + return (targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTargetType_.name), name); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_18__add_target_type___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_18__add_target_type___DB2.sql new file mode 100644 index 000000000..fed3796ff --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_18__add_target_type___DB2.sql @@ -0,0 +1,32 @@ +CREATE TABLE sp_target_type +( + id BIGINT GENERATED always AS IDENTITY NOT NULL, + tenant VARCHAR(40) NOT NULL, + colour VARCHAR(16), + created_at BIGINT NOT NULL, + created_by VARCHAR(64) NOT NULL, + description VARCHAR(512), + last_modified_at BIGINT NOT NULL, + last_modified_by VARCHAR(64) NOT NULL, + name VARCHAR(64) NOT NULL, + optlock_revision INTEGER, + PRIMARY KEY (id) +); + +CREATE INDEX sp_idx_target_type_prim + ON sp_target_type (tenant, id); + +CREATE TABLE sp_target_type_ds_type_relation +( + target_type BIGINT NOT NULL, + distribution_set_type BIGINT NOT NULL, + PRIMARY KEY (target_type, distribution_set_type) +); + +ALTER TABLE sp_target_type ADD CONSTRAINT uk_target_type_name UNIQUE (name, tenant); + +ALTER TABLE sp_target ADD COLUMN target_type BIGINT; +ALTER TABLE sp_target ADD CONSTRAINT fk_target_relation_target_type FOREIGN KEY (target_type) REFERENCES sp_target_type (id) ON DELETE SET NULL; + +ALTER TABLE sp_target_type_ds_type_relation ADD CONSTRAINT fk_target_type_relation_target_type FOREIGN KEY (target_type) REFERENCES sp_target_type (id) ON DELETE CASCADE; +ALTER TABLE sp_target_type_ds_type_relation ADD CONSTRAINT fk_target_type_relation_ds_type FOREIGN KEY (distribution_set_type) REFERENCES sp_distribution_set_type (id) ON DELETE CASCADE; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_18__add_target_type___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_18__add_target_type___H2.sql new file mode 100644 index 000000000..023b8fd96 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_18__add_target_type___H2.sql @@ -0,0 +1,50 @@ +create table sp_target_type +( + id bigint generated by default as identity, + created_at bigint, + created_by varchar(64), + last_modified_at bigint, + last_modified_by varchar(64), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) +); + +create table sp_target_type_ds_type_relation +( + target_type bigint not null, + distribution_set_type bigint not null, + primary key (target_type, distribution_set_type) +); + +alter table sp_target_type + add constraint uk_target_type_name unique (name, tenant); + +create index sp_idx_target_type_prim on sp_target_type (tenant, id); + +alter table sp_target + add column target_type bigint; + +alter table sp_target + add constraint fk_target_relation_target_type + foreign key (target_type) + references sp_target_type + on delete set null; + +alter table sp_target_type_ds_type_relation + add constraint fk_target_type_relation_target_type + foreign key (target_type) + references sp_target_type + on delete cascade; + +alter table sp_target_type_ds_type_relation + add constraint fk_target_type_relation_ds_type + foreign key (distribution_set_type) + references sp_distribution_set_type + on delete cascade; + + + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_18__add_target_type___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_18__add_target_type___MYSQL.sql new file mode 100644 index 000000000..c6435a63d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_18__add_target_type___MYSQL.sql @@ -0,0 +1,47 @@ +create table sp_target_type +( + id bigint not null auto_increment, + created_at bigint, + created_by varchar(64), + last_modified_at bigint, + last_modified_by varchar(64), + optlock_revision bigint, + tenant varchar(40) not null, + description varchar(512), + name varchar(64) not null, + colour varchar(16), + primary key (id) +); + +create table sp_target_type_ds_type_relation +( + target_type bigint not null, + distribution_set_type bigint not null, + primary key (target_type, distribution_set_type) +); + +alter table sp_target_type + add constraint uk_target_type_name unique (name, tenant); + +create index sp_idx_target_type_prim on sp_target_type (tenant, id); + +alter table sp_target + add column target_type bigint; + +alter table sp_target + add constraint fk_target_relation_target_type + foreign key (target_type) + references sp_target_type (id) + on delete set null; + +alter table sp_target_type_ds_type_relation + add constraint fk_target_type_relation_target_type + foreign key (target_type) + references sp_target_type (id) + on delete cascade; + +alter table sp_target_type_ds_type_relation + add constraint fk_target_type_relation_ds_type + foreign key (distribution_set_type) + references sp_distribution_set_type (id) + on delete cascade; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_18__add_target_type___POSTGRESQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_18__add_target_type___POSTGRESQL.sql new file mode 100644 index 000000000..8acd8038d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_18__add_target_type___POSTGRESQL.sql @@ -0,0 +1,65 @@ +-- ------------ Write CREATE-SEQUENCE-stage scripts ----------- + +CREATE SEQUENCE IF NOT EXISTS sp_target_type_seq +INCREMENT BY 1 +START WITH 1 +NO CYCLE; + +-- ------------ Write CREATE-TABLE-stage scripts ----------- + +CREATE TABLE sp_target_type( + id BIGINT NOT NULL DEFAULT nextval('sp_target_type_seq'), + created_at BIGINT, + created_by VARCHAR(64), + last_modified_at BIGINT, + last_modified_by VARCHAR(64), + optlock_revision BIGINT, + tenant VARCHAR(40) NOT NULL, + description VARCHAR(512), + name VARCHAR(64), + colour VARCHAR(16) +) + WITH ( + OIDS=FALSE + ); + +CREATE TABLE sp_target_type_ds_type_relation( + target_type BIGINT NOT NULL, + distribution_set_type BIGINT NOT NULL +) + WITH ( + OIDS=FALSE + ); + +-- ------------ Alter Table and Write INDEX scripts ----------- + +ALTER TABLE sp_target_type +ADD CONSTRAINT pk_sp_target_type PRIMARY KEY (id); + +ALTER TABLE sp_target_type +ADD CONSTRAINT uk_target_type_name UNIQUE (name, tenant); + +CREATE INDEX sp_idx_target_type_prim +ON sp_target_type +USING BTREE (tenant, id); + +ALTER TABLE sp_target +ADD COLUMN target_type BIGINT; + +ALTER TABLE sp_target +ADD CONSTRAINT fk_target_relation_target_type FOREIGN KEY (target_type) +REFERENCES sp_target_type (id) +ON UPDATE RESTRICT +ON DELETE SET NULL; + +ALTER TABLE sp_target_type_ds_type_relation +ADD CONSTRAINT fk_target_type_relation_target_type FOREIGN KEY (target_type) +REFERENCES sp_target_type (id) +ON UPDATE RESTRICT +ON DELETE CASCADE; + +ALTER TABLE sp_target_type_ds_type_relation +ADD CONSTRAINT fk_target_type_relation_ds_type FOREIGN KEY (distribution_set_type) +REFERENCES sp_distribution_set_type (id) +ON UPDATE RESTRICT +ON DELETE CASCADE; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_18__add_target_type___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_18__add_target_type___SQL_SERVER.sql new file mode 100644 index 000000000..924e0681c --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_18__add_target_type___SQL_SERVER.sql @@ -0,0 +1,27 @@ +CREATE TABLE sp_target_type +( + id NUMERIC(19) IDENTITY NOT NULL, + tenant VARCHAR(40) NOT NULL, + colour VARCHAR(16) NULL, + created_at NUMERIC(19) NOT NULL, + created_by VARCHAR(64) NOT NULL, + description VARCHAR(512) NULL, + last_modified_at NUMERIC(19) NOT NULL, + last_modified_by VARCHAR(64) NOT NULL, + name VARCHAR(64) NOT NULL, + optlock_revision INTEGER NULL, + PRIMARY KEY (id) +); +CREATE INDEX sp_idx_target_type_prim ON sp_target_type (tenant, id); +CREATE TABLE sp_target_type_ds_type_relation +( + target_type NUMERIC(19) NOT NULL, + distribution_set_type NUMERIC(19) NOT NULL, + PRIMARY KEY (target_type, distribution_set_type) +); +ALTER TABLE sp_target_type ADD CONSTRAINT uk_target_type_name UNIQUE (name, tenant); +ALTER TABLE sp_target ADD target_type NUMERIC(19) NULL; +ALTER TABLE sp_target ADD CONSTRAINT fk_target_relation_target_type FOREIGN KEY (target_type) REFERENCES sp_target_type (id) ON DELETE SET NULL; +ALTER TABLE sp_target_type_ds_type_relation ADD CONSTRAINT fk_target_type_relation_target_type FOREIGN KEY (target_type) REFERENCES sp_target_type (id) ON DELETE CASCADE; +ALTER TABLE sp_target_type_ds_type_relation ADD CONSTRAINT fk_target_type_relation_ds_type FOREIGN KEY (distribution_set_type) REFERENCES sp_distribution_set_type (id) ON DELETE CASCADE; + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java index 4b4cf1c05..d3c45fa1f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java @@ -76,6 +76,9 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest @Autowired protected TargetTagRepository targetTagRepository; + @Autowired + protected TargetTypeRepository targetTypeRepository; + @Autowired protected DistributionSetTagRepository distributionSetTagRepository; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index ae5549eea..de19b5575 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import javax.validation.ConstraintViolationException; @@ -27,6 +28,7 @@ import javax.validation.ConstraintViolationException; import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.FilterParams; +import org.eclipse.hawkbit.repository.builder.TargetUpdate; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; @@ -53,6 +55,7 @@ import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; @@ -1038,6 +1041,67 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { assertThat(updated.getTarget().getId()).isEqualTo(target.getId()); } + @Test + @WithUser(allSpPermissions = true) + @Description("Checks that target type for a target can be created, updated and unassigned.") + public void createAndUpdateTargetTypeInTarget() { + // create a target type + List targetTypes = testdataFactory.createTargetTypes("targettype", 2); + assertThat(targetTypes).hasSize(2); + // create a target + final Target target = testdataFactory.createTarget("target1", "testtarget", targetTypes.get(0).getId()); + // initial opt lock revision must be one + Optional targetFound = targetRepository.findById(target.getId()); + assertThat(targetFound).isPresent(); + assertThat(targetFound.get().getOptLockRevision()).isEqualTo(1); + assertThat(targetFound.get().getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); + + // update the target type + TargetUpdate targetUpdate = entityFactory.target().update(target.getControllerId()).targetType(targetTypes.get(1).getId()); + targetManagement.update(targetUpdate); + + // opt lock revision must be changed + Optional targetFound1 = targetRepository.findById(target.getId()); + assertThat(targetFound1).isPresent(); + assertThat(targetFound1.get().getOptLockRevision()).isEqualTo(2); + assertThat(targetFound1.get().getTargetType().getId()).isEqualTo(targetTypes.get(1).getId()); + + // unassign the target type + targetManagement.unAssignType(target.getControllerId()); + + // opt lock revision must be changed + Optional targetFound2 = targetRepository.findById(target.getId()); + assertThat(targetFound2).isPresent(); + assertThat(targetFound2.get().getOptLockRevision()).isEqualTo(3); + assertThat(targetFound2.get().getTargetType()).isNull(); + } + + @Test + @WithUser(allSpPermissions = true) + @Description("Checks that target type to a target can be assigned.") + public void assignTargetTypeInTarget() { + // create a target + final Target target = testdataFactory.createTarget("target1", "testtarget"); + // initial opt lock revision must be one + Optional targetFound = targetRepository.findById(target.getId()); + assertThat(targetFound).isPresent(); + assertThat(targetFound.get().getOptLockRevision()).isEqualTo(1); + assertThat(targetFound.get().getTargetType()).isNull(); + + // create a target type + TargetType targetType = testdataFactory.findOrCreateTargetType("targettype"); + assertThat(targetType).isNotNull(); + + // assign target type to target + targetManagement.assignType(targetFound.get().getControllerId(), targetType.getId()); + + // opt lock revision must be changed + Optional targetFound1 = targetRepository.findById(target.getId()); + assertThat(targetFound1).isPresent(); + assertThat(targetFound1.get().getOptLockRevision()).isEqualTo(2); + assertThat(targetFound1.get().getTargetType().getId()).isEqualTo(targetType.getId()); + } + @Test @Description("Queries and loads the metadata related to a given target.") public void findAllTargetMetadataByControllerId() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetTypeManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetTypeManagementTest.java new file mode 100644 index 000000000..1e9b7a740 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetTypeManagementTest.java @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Step; +import io.qameta.allure.Story; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeUpdatedEvent; +import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.repository.test.matcher.Expect; +import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; +import org.junit.jupiter.api.Test; + +import javax.validation.ConstraintViolationException; +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Feature("Component Tests - Repository") +@Story("Target Type Management") +public class TargetTypeManagementTest extends AbstractJpaIntegrationTest{ + + @Test + @Description("Verifies that management get access react as specified on calls for non existing entities by means " + + "of Optional not present.") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 0) }) + public void nonExistingEntityAccessReturnsNotPresent() { + assertThat(targetTypeManagement.get(NOT_EXIST_IDL)).isNotPresent(); + assertThat(targetTypeManagement.getByName(NOT_EXIST_ID)).isNotPresent(); + } + + @Test + @Description("Verifies that management queries react as specified on calls for non existing entities " + + " by means of throwing EntityNotFoundException.") + @ExpectEvents({ @Expect(type = TargetTypeUpdatedEvent.class, count = 0) }) + public void entityQueriesReferringToNotExistingEntitiesThrowsException() { + verifyThrownExceptionBy(() -> targetTypeManagement.delete(NOT_EXIST_IDL), "TargetType"); + verifyThrownExceptionBy(() -> targetTypeManagement.update(entityFactory.targetType().update(NOT_EXIST_IDL)), + "TargetType"); + } + + @Test + @Description("Verify that a target type with invalid properties cannot be created or updated") + public void createAndUpdateTargetTypeWithInvalidFields() { + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype1").description("targettypedes1")); + + createAndUpdateTargetTypeWithInvalidDescription(targetType); + createAndUpdateTargetTypeWithInvalidColour(targetType); + createAndUpdateTargetTypeWithInvalidName(targetType); + } + + @Step + private void createAndUpdateTargetTypeWithInvalidDescription(final TargetType targetType) { + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.create( + entityFactory.targetType().create().name("a").description(RandomStringUtils.randomAlphanumeric(TargetType.DESCRIPTION_MAX_SIZE + 1)))) + .as("targetType with too long description should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> targetTypeManagement.create(entityFactory.targetType().create().name("a").description(INVALID_TEXT_HTML))) + .as("targetType with invalid description should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.update( + entityFactory.targetType().update(targetType.getId()).description(RandomStringUtils.randomAlphanumeric(TargetType.DESCRIPTION_MAX_SIZE + 1)))) + .as("targetType with too long description should not be updated"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement + .update(entityFactory.targetType().update(targetType.getId()).description(INVALID_TEXT_HTML))) + .as("targetType with invalid description should not be updated"); + } + + @Step + private void createAndUpdateTargetTypeWithInvalidColour(final TargetType targetType) { + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.create( + entityFactory.targetType().create().name("a").colour(RandomStringUtils.randomAlphanumeric(TargetType.COLOUR_MAX_SIZE + 1)))) + .as("targetType with too long colour should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> targetTypeManagement.create(entityFactory.targetType().create().name("a").colour(INVALID_TEXT_HTML))) + .as("targetType with invalid colour should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.update( + entityFactory.targetType().update(targetType.getId()).colour(RandomStringUtils.randomAlphanumeric(TargetType.COLOUR_MAX_SIZE + 1)))) + .as("targetType with too long colour should not be updated"); + + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> targetTypeManagement.update(entityFactory.targetType().update(targetType.getId()).colour(INVALID_TEXT_HTML))) + .as("targetType with invalid colour should not be updated"); + } + + @Step + private void createAndUpdateTargetTypeWithInvalidName(final TargetType targetType) { + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement + .create(entityFactory.targetType().create().name(RandomStringUtils.randomAlphanumeric( + NamedEntity.NAME_MAX_SIZE + 1)))) + .as("targetType with too long name should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.create(entityFactory.targetType().create().name(INVALID_TEXT_HTML))) + .as("targetType with invalid name should not be created"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement + .update(entityFactory.targetType().update(targetType.getId()).name(RandomStringUtils.randomAlphanumeric( + NamedEntity.NAME_MAX_SIZE + 1)))) + .as("targetType with too long name should not be updated"); + + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> targetTypeManagement.update(entityFactory.targetType().update(targetType.getId()).name(INVALID_TEXT_HTML))) + .as("targetType with invalid name should not be updated"); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> targetTypeManagement.update(entityFactory.targetType().update(targetType.getId()).name(""))) + .as("targetType with too short name should not be updated"); + + } + + @Test + @Description("Tests the successful assignment of compatible distribution set types to a target type") + public void assignCompatibleDistributionSetTypesToTargetType(){ + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype1").description("targettypedes1")); + DistributionSetType distributionSetType = testdataFactory.findOrCreateDistributionSetType("testDst", "dst1"); + targetTypeManagement.assignCompatibleDistributionSetTypes(targetType.getId(), Collections.singletonList(distributionSetType.getId())); + + Optional targetTypeWithDsTypes = targetTypeRepository.findById(targetType.getId()); + assertThat(targetTypeWithDsTypes).isPresent(); + assertThat(targetTypeWithDsTypes.get().getCompatibleDistributionSetTypes()).extracting("key").contains("testDst"); + } + + @Test + @Description("Tests the successful removal of compatible distribution set types to a target type") + public void unassignCompatibleDistributionSetTypesToTargetType(){ + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype11").description("targettypedes11")); + DistributionSetType distributionSetType = testdataFactory.findOrCreateDistributionSetType("testDst1", "dst11"); + targetTypeManagement.assignCompatibleDistributionSetTypes(targetType.getId(), Collections.singletonList(distributionSetType.getId())); + Optional targetTypeWithDsTypes = targetTypeRepository.findById(targetType.getId()); + assertThat(targetTypeWithDsTypes).isPresent(); + assertThat(targetTypeWithDsTypes.get().getCompatibleDistributionSetTypes()).extracting("key").contains("testDst1"); + targetTypeManagement.unassignDistributionSetType(targetType.getId(),distributionSetType.getId()); + Optional targetTypeWithDsTypes1 = targetTypeRepository.findById(targetType.getId()); + assertThat(targetTypeWithDsTypes1).isPresent(); + assertThat(targetTypeWithDsTypes1.get().getCompatibleDistributionSetTypes()).hasSize(0); + } + + @Test + @Description("Ensures that all types are retrieved through repository.") + public void findAllTargetTypes() { + testdataFactory.createTargetTypes("targettype", 10); + assertThat(targetTypeRepository.findAll()).as("Target type size").hasSize(10); + } + + @Test + @Description("Ensures that a created target type is persisted in the repository as defined.") + public void createTargetType() { + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype1").description("targettypedes1").colour("colour1")); + + assertThat(targetTypeRepository.findByName("targettype1").get().getDescription()).as("type found") + .isEqualTo("targettypedes1"); + assertThat(targetTypeManagement.getByName("targettype1").get().getColour()).as("type found").isEqualTo("colour1"); + assertThat(targetTypeManagement.get(targetType.getId()).get().getColour()).as("type found").isEqualTo("colour1"); + } + + @Test + @Description("Ensures that a deleted target type is removed from the repository as defined.") + public void deleteTargetType() { + // create test data + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype11").description("targettypedes11")); + assertThat(targetTypeRepository.findByName("targettype11").get().getDescription()).as("type found") + .isEqualTo("targettypedes11"); + targetTypeManagement.delete(targetType.getId()); + assertThat(targetTypeRepository.findById(targetType.getId())).as("No target type should be found").isNotPresent(); + + } + + @Test + @Description("Tests the name update of a target type.") + public void updateTargetType() { + final TargetType targetType = targetTypeManagement + .create(entityFactory.targetType().create().name("targettype111").description("targettypedes111")); + assertThat(targetTypeRepository.findByName("targettype111").get().getDescription()).as("type found") + .isEqualTo("targettypedes111"); + targetTypeManagement.update(entityFactory.targetType().update(targetType.getId()).name("updatedtargettype111")); + assertThat(targetTypeRepository.findByName("updatedtargettype111")).as("Updated target type should be found").isPresent(); + } + + @Test + @Description("Ensures that a target type cannot be created if one exists already with that name (expects EntityAlreadyExistsException).") + public void failedDuplicateTargetTypeNameException() { + targetTypeManagement.create(entityFactory.targetType().create().name("targettype123")); + assertThrows(EntityAlreadyExistsException.class, () -> targetTypeManagement.create(entityFactory.targetType().create().name("targettype123"))); + } + + @Test + @Description("Ensures that a target type cannot be updated to a name that already exists (expects EntityAlreadyExistsException).") + public void failedDuplicateTargetTypeNameExceptionAfterUpdate() { + targetTypeManagement.create(entityFactory.targetType().create().name("targettype1234")); + TargetType targetType = targetTypeManagement.create(entityFactory.targetType().create().name("targettype12345")); + assertThrows(EntityAlreadyExistsException.class, () -> targetTypeManagement.update(entityFactory.targetType().update(targetType.getId()).name("targettype1234"))); + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/event/RepositoryEntityEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/event/RepositoryEntityEventTest.java index ced4021d2..f049acc48 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/event/RepositoryEntityEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/event/RepositoryEntityEventTest.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.jpa.event; import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -20,10 +21,13 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.RolloutDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; +import org.eclipse.hawkbit.repository.event.remote.TargetTypeDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.event.RepositoryEntityEventTest.RepositoryTestConfiguration; @@ -31,6 +35,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -88,6 +93,38 @@ public class RepositoryEntityEventTest extends AbstractJpaIntegrationTest { assertThat(targetDeletedEvent.getEntityId()).isEqualTo(createdTarget.getId()); } + @Test + @Description("Verifies that the target type created event is published when a target type has been created") + public void targetTypeCreatedEventIsPublished() throws InterruptedException { + TargetType createdTargetType = testdataFactory.findOrCreateTargetType("targettype"); + + TargetTypeCreatedEvent targetTypeCreatedEvent = eventListener.waitForEvent(TargetTypeCreatedEvent.class); + assertThat(targetTypeCreatedEvent).isNotNull(); + assertThat(targetTypeCreatedEvent.getEntity().getId()).isEqualTo(createdTargetType.getId()); + } + + @Test + @Description("Verifies that the target type updated event is published when a target type has been updated") + public void targetTypeUpdatedEventIsPublished() throws InterruptedException { + TargetType createdTargetType = testdataFactory.findOrCreateTargetType("targettype"); + targetTypeManagement.update(entityFactory.targetType().update(createdTargetType.getId()).name("updatedtargettype")); + + TargetTypeUpdatedEvent targetTypeUpdatedEvent = eventListener.waitForEvent(TargetTypeUpdatedEvent.class); + assertThat(targetTypeUpdatedEvent).isNotNull(); + assertThat(targetTypeUpdatedEvent.getEntity().getId()).isEqualTo(createdTargetType.getId()); + } + + @Test + @Description("Verifies that the target type deleted event is published when a target type has been deleted") + public void targetTypeDeletedEventIsPublished() throws InterruptedException { + TargetType createdTargetType = testdataFactory.findOrCreateTargetType("targettype"); + targetTypeManagement.delete(createdTargetType.getId()); + + TargetTypeDeletedEvent targetTypeDeletedEvent = eventListener.waitForEvent(TargetTypeDeletedEvent.class); + assertThat(targetTypeDeletedEvent).isNotNull(); + assertThat(targetTypeDeletedEvent.getEntityId()).isEqualTo(createdTargetType.getId()); + } + @Test @Description("Verifies that the rollout deleted event is published when a rollout has been deleted") public void rolloutDeletedEventIsPublished() throws InterruptedException { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 9937a9c28..860d14e66 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -44,6 +44,7 @@ import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -144,6 +145,9 @@ public abstract class AbstractIntegrationTest { @Autowired protected TargetManagement targetManagement; + @Autowired + protected TargetTypeManagement targetTypeManagement; + @Autowired protected TargetFilterQueryManagement targetFilterQueryManagement; diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/JpaTestRepositoryManagement.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/JpaTestRepositoryManagement.java index 894e1d302..80309c8f8 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/JpaTestRepositoryManagement.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/JpaTestRepositoryManagement.java @@ -63,7 +63,7 @@ public class JpaTestRepositoryManagement { return null; }); } catch (final Exception e) { - LOGGER.error("Error hile delete tenant", e); + LOGGER.error("Error while delete tenant", e); } }); } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 54fbaab89..4f356a336 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -37,8 +37,10 @@ import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.builder.TagCreate; import org.eclipse.hawkbit.repository.builder.TargetCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; @@ -61,6 +63,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; @@ -123,6 +126,8 @@ public class TestdataFactory { */ public static final String SM_TYPE_APP = "application"; + public static final String DEFAULT_COLOUR = "#000000"; + @Autowired private ControllerManagement controllerManagament; @@ -141,6 +146,9 @@ public class TestdataFactory { @Autowired private TargetManagement targetManagement; + @Autowired + private TargetTypeManagement targetTypeManagement; + @Autowired private DeploymentManagement deploymentManagement; @@ -638,6 +646,22 @@ public class TestdataFactory { return target; } + /** + * @param controllerId + * of the target + * @param targetName + * name of the target + * @param targetTypeId + * target type id + * @return persisted {@link Target} + */ + public Target createTarget(final String controllerId, final String targetName, final Long targetTypeId) { + final Target target = targetManagement + .create(entityFactory.target().create().controllerId(controllerId).name(targetName).targetType(targetTypeId)); + assertTargetProperlyCreated(target); + return target; + } + private void assertTargetProperlyCreated(final Target target) { assertThat(target.getCreatedBy()).isNotNull(); assertThat(target.getCreatedAt()).isNotNull(); @@ -1160,4 +1184,54 @@ public class TestdataFactory { rolloutManagement.handleRollouts(); return newRollout; } + + /** + * Finds {@link TargetType} in repository with given + * {@link TargetType#getName()} or creates if it does not exist yet. + * No ds types are assigned on creation. + * + * @param targetTypeName + * {@link TargetType#getName()} + * + * @return persisted {@link TargetType} + */ + public TargetType findOrCreateTargetType(final String targetTypeName) { + return targetTypeManagement.getByName(targetTypeName) + .orElseGet(() -> targetTypeManagement.create(entityFactory.targetType().create() + .name(targetTypeName).description(targetTypeName + " description").colour(DEFAULT_COLOUR))); + } + + /** + * Creates {@link TargetType} in repository with given + * {@link TargetType#getName()}. Compatible distribution set types are assigned on creation + * + * @param targetTypeName + * {@link TargetType#getName()} + * + * @return persisted {@link TargetType} + */ + public TargetType createTargetType(final String targetTypeName, List compatibleDsTypes) { + return targetTypeManagement.create(entityFactory.targetType().create() + .name(targetTypeName).description(targetTypeName + " description").colour(DEFAULT_COLOUR) + .compatible(compatibleDsTypes.stream().map(DistributionSetType::getId).collect(Collectors.toList()))); + } + + /** + * Creates {@link TargetType} in repository with given + * {@link TargetType#getName()}. No ds types are assigned on creation. + * + * @param targetTypePrefix + * {@link TargetType#getName()} + * + * @return persisted {@link TargetType} + */ + public List createTargetTypes(final String targetTypePrefix, int count) { + final List result = Lists.newArrayListWithExpectedSize(count); + for (int i = 0; i < count; i++) { + result.add(entityFactory.targetType().create().name(targetTypePrefix + i).description(targetTypePrefix + " description") + .colour(DEFAULT_COLOUR)); + } + return targetTypeManagement.create(result); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionsettype/MgmtDistributionSetTypeAssignment.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionsettype/MgmtDistributionSetTypeAssignment.java new file mode 100644 index 000000000..c838c9849 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionsettype/MgmtDistributionSetTypeAssignment.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.distributionsettype; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.eclipse.hawkbit.mgmt.json.model.MgmtId; + +/** + * Request Body of DistributionSetType for assignment operations (ID only). + * + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MgmtDistributionSetTypeAssignment extends MgmtId { +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTarget.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTarget.java index 7553cb947..49fd2ca11 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTarget.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTarget.java @@ -49,6 +49,24 @@ public class MgmtTarget extends MgmtNamedEntity { @JsonProperty private boolean requestAttributes; + @JsonProperty + private Long targetType; + + /** + * @return Target type ID + */ + public Long getTargetType() { + return targetType; + } + + /** + * @param targetType + * Target type ID + */ + public void setTargetType(Long targetType) { + this.targetType = targetType; + } + /** * @return the controllerId */ @@ -150,6 +168,9 @@ public class MgmtTarget extends MgmtNamedEntity { return securityToken; } + /** + * @return Address + */ public String getAddress() { return address; } @@ -171,6 +192,9 @@ public class MgmtTarget extends MgmtNamedEntity { this.securityToken = securityToken; } + /** + * @return boolean true or false + */ public boolean isRequestAttributes() { return requestAttributes; } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTargetRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTargetRequestBody.java index e526ad44e..2c7541f0f 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTargetRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtTargetRequestBody.java @@ -27,10 +27,34 @@ public class MgmtTargetRequestBody { @JsonProperty private Boolean requestAttributes; + @JsonProperty + private Long targetType; + + /** + * @return Target type ID + */ + public Long getTargetType() { + return targetType; + } + + /** + * @param targetType + * Target type ID + */ + public void setTargetType(Long targetType) { + this.targetType = targetType; + } + + /** + * @return token + */ public String getSecurityToken() { return securityToken; } + /** + * @param securityToken Token + */ public void setSecurityToken(final String securityToken) { this.securityToken = securityToken; } @@ -83,18 +107,32 @@ public class MgmtTargetRequestBody { return this; } + /** + * @return address + */ public String getAddress() { return address; } + /** + * @param address + * Address + */ public void setAddress(final String address) { this.address = address; } + /** + * @return boolean true or false + */ public Boolean isRequestAttributes() { return requestAttributes; } + /** + * @param requestAttributes + * Attributes + */ public void setRequestAttributes(final Boolean requestAttributes) { this.requestAttributes = requestAttributes; } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetType.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetType.java new file mode 100644 index 000000000..9c3f432ad --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetType.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.targettype; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.eclipse.hawkbit.mgmt.json.model.MgmtNamedEntity; + +/** + * A json annotated rest model for TargetType to RESTful API + * representation. + * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MgmtTargetType extends MgmtNamedEntity { + + @JsonProperty(value = "id", required = true) + private Long typeId; + + @JsonProperty + private String colour; + + /** + * @return target type ID + */ + public Long getTypeId() { + return typeId; + } + + /** + * @param typeId + * Target type ID + */ + public void setTypeId(final Long typeId) { + this.typeId = typeId; + } + + /** + * + * @return the color in format #000000 + */ + public String getColour() { + return colour; + } + + /** + * @param colour + * in format #000000 + */ + public void setColour(String colour) { + this.colour = colour; + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPost.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPost.java new file mode 100644 index 000000000..8adc15c72 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPost.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.targettype; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeAssignment; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleTypeAssigment; + +/** + * Request Body for TargetType POST. + * + */ +public class MgmtTargetTypeRequestBodyPost extends MgmtTargetTypeRequestBodyPut{ + + @JsonProperty + private List compatibledistributionsettypes; + + /** + * @param name + * the name to set + * @return post request body + */ + @Override + public MgmtTargetTypeRequestBodyPost setName(final String name) { + super.setName(name); + return this; + } + + @Override + public MgmtTargetTypeRequestBodyPost setDescription(final String description) { + super.setDescription(description); + return this; + } + + @Override + public MgmtTargetTypeRequestBodyPost setColour(final String colour) { + super.setColour(colour); + return this; + } + + + /** + * @return the compatible ds types + */ + public List getCompatibleDsTypes() { + return compatibledistributionsettypes; + } + + /** + * @param compatibleDsTypes + * the compatible ds types to set + * + * @return updated body + */ + public MgmtTargetTypeRequestBodyPost setCompatibleDsTypes( + final List compatibleDsTypes) { + this.compatibledistributionsettypes = compatibleDsTypes; + return this; + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPut.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPut.java new file mode 100644 index 000000000..139116653 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/targettype/MgmtTargetTypeRequestBodyPut.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model.targettype; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request Body for TargetType PUT. + * + */ +public class MgmtTargetTypeRequestBodyPut { + + @JsonProperty(required = true) + private String name; + + @JsonProperty + private String description; + + @JsonProperty + private String colour; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + * + * @return updated body + */ + public MgmtTargetTypeRequestBodyPut setName(final String name) { + this.name = name; + return this; + } + + /** + * @return description + */ + public String getDescription() { + return description; + } + + /** + * @param description + * Description + * @return Updated body + */ + public MgmtTargetTypeRequestBodyPut setDescription(final String description) { + this.description = description; + return this; + } + + /** + * @return Colour + */ + public String getColour() { + return colour; + } + + /** + * @param colour + * Colour + * @return Updated body + */ + public MgmtTargetTypeRequestBodyPut setColour(final String colour) { + this.colour = colour; + return this; + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java index cbb7bf8ed..bd742ccf7 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtRestConstants.java @@ -60,6 +60,11 @@ public final class MgmtRestConstants { public static final String SYSTEM_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + BASE_SYSTEM_MAPPING; + /** + * The target URL mapping, href link for assigned target type. + */ + public static final String TARGET_V1_ASSIGNED_TARGET_TYPE= "targetType"; + /** * The target URL mapping, href link for assigned distribution set. */ @@ -99,6 +104,21 @@ public final class MgmtRestConstants { */ public static final String TARGET_TAG_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/targettags"; + /** + * The target URL mapping rest resource. + */ + public static final String TARGET_TARGET_TYPE_V1_REQUEST_MAPPING = "/{targetId}/targettype"; + + /** + * The target type URL mapping rest resource. + */ + public static final String TARGETTYPE_V1_REQUEST_MAPPING = BASE_V1_REQUEST_MAPPING + "/targettypes"; + + /** + * The target type URL mapping rest resource. + */ + public static final String TARGETTYPE_V1_DS_TYPES = "compatibledistributionsettypes"; + /** * The tag URL mapping rest resource. * diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java index e51cfd19e..dd1c107ba 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetRestApi.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.mgmt.rest.api; import java.util.List; +import org.eclipse.hawkbit.mgmt.json.model.MgmtId; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; @@ -123,6 +124,31 @@ public interface MgmtTargetRestApi { @DeleteMapping(value = "/{targetId}") ResponseEntity deleteTarget(@PathVariable("targetId") String targetId); + /** + * Handles the DELETE (unassign) request of a target type. + * + * @param targetId + * the ID of the target + * @return If the given targetId could exists and could be unassign Http OK. + * In any failure the JsonResponseExceptionHandler is handling the + * response. + */ + @DeleteMapping(value = MgmtRestConstants.TARGET_TARGET_TYPE_V1_REQUEST_MAPPING) + ResponseEntity unassignTargetType(@PathVariable("targetId") String targetId); + + /** + * Handles the POST (assign) request of a target type. + * + * @param targetId + * the ID of the target + * @return If the given targetId could exists and could be assign Http OK. + * In any failure the JsonResponseExceptionHandler is handling the + * response. + */ + @PostMapping(value = MgmtRestConstants.TARGET_TARGET_TYPE_V1_REQUEST_MAPPING, consumes = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity assignTargetType(@PathVariable("targetId") String targetId, MgmtId targetTypeId); + /** * Handles the GET request of retrieving the attributes of a specific * target. diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetTypeRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetTypeRestApi.java new file mode 100644 index 000000000..f221e5248 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTargetTypeRestApi.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.rest.api; + +import java.util.List; + +import org.eclipse.hawkbit.mgmt.json.model.PagedList; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeAssignment; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetTypeRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetTypeRequestBodyPut; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * REST Resource handling for TargetType CRUD operations. + * + */ +@RequestMapping(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING) +public interface MgmtTargetTypeRestApi { + + /** + * Handles the GET request of retrieving all TargetTypes. + * + * @param pagingOffsetParam + * the offset of list of target types for pagination, might not be + * present in the rest request then default value will be applied + * @param pagingLimitParam + * the limit of the paged request, might not be present in the rest + * request then default value will be applied + * @param sortParam + * the sorting parameter in the request URL, syntax + * {@code field:direction, field:direction} + * @param rsqlParam + * the search parameter in the request URL, syntax + * {@code q=name==abc} + * + * @return a list of all TargetTypes for a defined or default page request with + * status OK. The response is always paged. In any failure the + * JsonResponseExceptionHandler is handling the response. + */ + @GetMapping(produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> getTargetTypes( + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) String rsqlParam); + + /** + * Handles the GET request of retrieving a single TargetType. + * + * @param targetTypeId + * the ID of the target type to retrieve + * + * @return a single target type with status OK. + */ + @GetMapping(value = "/{targetTypeId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity getTargetType(@PathVariable("targetTypeId") Long targetTypeId); + + /** + * Handles the DELETE request for a single Target Type. + * + * @param targetTypeId + * the ID of the target type to retrieve + * @return status OK if delete is successful. + * + */ + @DeleteMapping(value = "/{targetTypeId}") + ResponseEntity deleteTargetType(@PathVariable("targetTypeId") Long targetTypeId); + + /** + * Handles the PUT request of updating a Target Type. + * + * @param targetTypeId + * the ID of the target type in the URL + * @param restTargetType + * the target type to be updated. + * @return status OK if update is successful + */ + @PutMapping(value = "/{targetTypeId}", consumes = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity updateTargetType(@PathVariable("targetTypeId") Long targetTypeId, + MgmtTargetTypeRequestBodyPut restTargetType); + + /** + * Handles the POST request of creating new Target Types. The request body must + * always be a list of types. + * + * @param targetTypes + * the target types to be created. + * @return In case all target types could be successfully created the + * ResponseEntity with status code 201 - Created but without + * ResponseBody. In any failure the JsonResponseExceptionHandler is + * handling the response. + */ + @PostMapping(consumes = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> createTargetTypes(List targetTypes); + + /** + * Handles the GET request of retrieving the list of compatible distribution set + * types in that target type. + * + * @param targetTypeId + * of the TargetType. + * @return Unpaged list of distribution set types and OK in case of success. + */ + @GetMapping(value = "/{targetTypeId}/" + MgmtRestConstants.TARGETTYPE_V1_DS_TYPES, produces = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity> getCompatibleDistributionSets( + @PathVariable("targetTypeId") Long targetTypeId); + + /** + * Handles DELETE request for removing the compatibility of a distribution set + * type from the target type. + * + * @param targetTypeId + * of the TargetType. + * @param distributionSetTypeId + * of the DistributionSetType. + * + * @return OK if the request was successful + */ + @DeleteMapping(value = "/{targetTypeId}/" + MgmtRestConstants.TARGETTYPE_V1_DS_TYPES + "/{distributionSetTypeId}") + ResponseEntity removeCompatibleDistributionSet(@PathVariable("targetTypeId") Long targetTypeId, + @PathVariable("distributionSetTypeId") Long distributionSetTypeId); + + /** + * Handles the POST request for adding the compatibility of a distribution set + * type to a target type. + * + * @param targetTypeId + * of the TargetType. + * @param distributionSetTypeIds + * of the DistributionSetTypes as a List. + * + * @return OK if the request was successful + */ + @PostMapping(value = "/{targetTypeId}/" + MgmtRestConstants.TARGETTYPE_V1_DS_TYPES, consumes = { + MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }) + ResponseEntity addCompatibleDistributionSets(@PathVariable("targetTypeId") final Long targetTypeId, + final List distributionSetTypeIds); +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeMapper.java index dfd3b2896..7f438eaae 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeMapper.java @@ -67,7 +67,7 @@ final class MgmtDistributionSetTypeMapper { .orElse(Collections.emptyList()); } - static List toListResponse(final List types) { + static List toListResponse(final Collection types) { if (types == null) { return Collections.emptyList(); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index bc6162a14..e2c1b9fec 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; @@ -30,6 +31,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi; import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.ActionStatusFields; import org.eclipse.hawkbit.repository.DeploymentManagement; @@ -79,6 +81,10 @@ public final class MgmtTargetMapper { response.add(linkTo(methodOn(MgmtTargetRestApi.class).getMetadata(response.getControllerId(), MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET_VALUE, MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT_VALUE, null, null)).withRel("metadata")); + if (response.getTargetType() != null) { + response.add(linkTo(methodOn(MgmtTargetTypeRestApi.class) + .getTargetType(response.getTargetType())).withRel(MgmtRestConstants.TARGET_V1_ASSIGNED_TARGET_TYPE)); + } } static void addPollStatus(final Target target, final MgmtTarget targetRest) { @@ -153,6 +159,9 @@ public final class MgmtTargetMapper { if (installationDate != null) { targetRest.setInstalledAt(installationDate); } + if (target.getTargetType() != null){ + targetRest.setTargetType(target.getTargetType().getId()); + } targetRest.add(linkTo(methodOn(MgmtTargetRestApi.class).getTarget(target.getControllerId())).withSelfRel()); @@ -172,7 +181,7 @@ public final class MgmtTargetMapper { private static TargetCreate fromRequest(final EntityFactory entityFactory, final MgmtTargetRequestBody targetRest) { return entityFactory.target().create().controllerId(targetRest.getControllerId()).name(targetRest.getName()) .description(targetRest.getDescription()).securityToken(targetRest.getSecurityToken()) - .address(targetRest.getAddress()); + .address(targetRest.getAddress()).targetType(targetRest.getTargetType()); } static List fromRequestTargetMetadata(final List metadata, diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index cc750fe09..f7b1e4f04 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import javax.validation.Valid; import javax.validation.ValidationException; +import org.eclipse.hawkbit.mgmt.json.model.MgmtId; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; @@ -140,7 +141,8 @@ public class MgmtTargetResource implements MgmtTargetRestApi { final Target updateTarget = this.targetManagement.update(entityFactory.target().update(targetId) .name(targetRest.getName()).description(targetRest.getDescription()).address(targetRest.getAddress()) - .securityToken(targetRest.getSecurityToken()).requestAttributes(targetRest.isRequestAttributes())); + .targetType(targetRest.getTargetType()).securityToken(targetRest.getSecurityToken()) + .requestAttributes(targetRest.isRequestAttributes())); final MgmtTarget response = MgmtTargetMapper.toResponse(updateTarget); MgmtTargetMapper.addPollStatus(updateTarget, response); @@ -156,6 +158,18 @@ public class MgmtTargetResource implements MgmtTargetRestApi { return ResponseEntity.ok().build(); } + @Override + public ResponseEntity unassignTargetType(@PathVariable("targetId") final String targetId) { + this.targetManagement.unAssignType(targetId); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity assignTargetType(@PathVariable("targetId") final String targetId, @RequestBody final MgmtId targetTypeId) { + this.targetManagement.assignType(targetId, targetTypeId.getId()); + return ResponseEntity.ok().build(); + } + @Override public ResponseEntity getAttributes(@PathVariable("targetId") final String targetId) { final Map controllerAttributes = targetManagement.getControllerAttributes(targetId); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeMapper.java new file mode 100644 index 000000000..ab5dda58e --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeMapper.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.rest.resource; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeAssignment; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetTypeRequestBodyPost; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.rest.data.ResponseList; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +/** + * A mapper which maps repository model to RESTful model representation and + * back. + * + */ +public final class MgmtTargetTypeMapper { + + // private constructor, utility class + private MgmtTargetTypeMapper() { + } + + static List targetFromRequest(final EntityFactory entityFactory, + final Collection targetTypesRest) { + if (targetTypesRest == null) { + return Collections.emptyList(); + } + return targetTypesRest.stream().map(targetRest -> fromRequest(entityFactory, targetRest)).collect(Collectors.toList()); + } + + private static TargetTypeCreate fromRequest(final EntityFactory entityFactory, + final MgmtTargetTypeRequestBodyPost targetTypesRest) { + return entityFactory.targetType().create().name(targetTypesRest.getName()).description(targetTypesRest.getDescription()) + .colour(targetTypesRest.getColour()).compatible(getDistributionSets(targetTypesRest)); + } + + private static Collection getDistributionSets(final MgmtTargetTypeRequestBodyPost targetTypesRest) { + return Optional.ofNullable(targetTypesRest.getCompatibleDsTypes()) + .map(ds -> ds.stream().map(MgmtDistributionSetTypeAssignment::getId).collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } + + static List toListResponse(final List types) { + if (types == null) { + return Collections.emptyList(); + } + return new ResponseList<>(types.stream().map(MgmtTargetTypeMapper::toResponse).collect(Collectors.toList())); + } + + static MgmtTargetType toResponse(final TargetType type) { + final MgmtTargetType result = new MgmtTargetType(); + MgmtRestModelMapper.mapNamedToNamed(result, type); + result.setTypeId(type.getId()); + result.setColour(type.getColour()); + result.add(linkTo(methodOn(MgmtTargetTypeRestApi.class).getTargetType(result.getTypeId())).withSelfRel()); + return result; + } + + static void addLinks(final MgmtTargetType result) { + result.add(linkTo(methodOn(MgmtTargetTypeRestApi.class).getCompatibleDistributionSets(result.getTypeId())) + .withRel(MgmtRestConstants.TARGETTYPE_V1_DS_TYPES)); + } +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResource.java new file mode 100644 index 000000000..029b0023b --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResource.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.rest.resource; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.mgmt.json.model.PagedList; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType; +import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetTypeAssignment; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetTypeRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetTypeRequestBodyPut; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi; +import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; +import org.eclipse.hawkbit.repository.TargetTypeManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST Resource handling for {@link TargetType} CRUD operations. + */ +@RestController +public class MgmtTargetTypeResource implements MgmtTargetTypeRestApi { + private static final Logger LOG = LoggerFactory.getLogger(MgmtTargetTypeResource.class); + + private final TargetTypeManagement targetTypeManagement; + private final EntityFactory entityFactory; + + public MgmtTargetTypeResource(TargetTypeManagement targetTypeManagement, final EntityFactory entityFactory) { + this.targetTypeManagement = targetTypeManagement; + this.entityFactory = entityFactory; + } + + @Override + public ResponseEntity> getTargetTypes( + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_OFFSET) final int pagingOffsetParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, defaultValue = MgmtRestConstants.REQUEST_PARAMETER_PAGING_DEFAULT_LIMIT) final int pagingLimitParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SORTING, required = false) final String sortParam, + @RequestParam(value = MgmtRestConstants.REQUEST_PARAMETER_SEARCH, required = false) final String rsqlParam) { + + final int sanitizedOffsetParam = PagingUtility.sanitizeOffsetParam(pagingOffsetParam); + final int sanitizedLimitParam = PagingUtility.sanitizePageLimitParam(pagingLimitParam); + final Sort sorting = PagingUtility.sanitizeTargetTypeSortParam(sortParam); + final Pageable pageable = new OffsetBasedPageRequest(sanitizedOffsetParam, sanitizedLimitParam, sorting); + + final Page findTargetTypesAll; + long countTargetTypesAll; + if (rsqlParam != null) { + findTargetTypesAll= targetTypeManagement.findByRsql(pageable, rsqlParam); + } else { + findTargetTypesAll = targetTypeManagement.findAll(pageable); + } + countTargetTypesAll = findTargetTypesAll.getTotalElements(); + + final List rest = MgmtTargetTypeMapper.toListResponse(findTargetTypesAll.getContent()); + return ResponseEntity.ok(new PagedList<>(rest, countTargetTypesAll)); + } + + @Override + public ResponseEntity getTargetType(@PathVariable("targetTypeId") final Long targetTypeId) { + final TargetType foundType = findTargetTypeWithExceptionIfNotFound(targetTypeId); + final MgmtTargetType response = MgmtTargetTypeMapper.toResponse(foundType); + MgmtTargetTypeMapper.addLinks(response); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity deleteTargetType(@PathVariable("targetTypeId") final Long targetTypeId) { + LOG.debug("Delete {} target type", targetTypeId); + targetTypeManagement.delete(targetTypeId); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity updateTargetType(@PathVariable("targetTypeId") final Long targetTypeId, + @RequestBody final MgmtTargetTypeRequestBodyPut restTargetType) { + + final TargetType updated = targetTypeManagement + .update(entityFactory.targetType().update(targetTypeId).name(restTargetType.getName()) + .description(restTargetType.getDescription()).colour(restTargetType.getColour())); + final MgmtTargetType response = MgmtTargetTypeMapper.toResponse(updated); + MgmtTargetTypeMapper.addLinks(response); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity> createTargetTypes( + @RequestBody final List targetTypes) { + + final List createdTargetTypes = targetTypeManagement + .create(MgmtTargetTypeMapper.targetFromRequest(entityFactory, targetTypes)); + return ResponseEntity.status(HttpStatus.CREATED).body(MgmtTargetTypeMapper.toListResponse(createdTargetTypes)); + } + + @Override + public ResponseEntity> getCompatibleDistributionSets( + @PathVariable("targetTypeId") final Long targetTypeId) { + + final TargetType foundType = findTargetTypeWithExceptionIfNotFound(targetTypeId); + return ResponseEntity + .ok(MgmtDistributionSetTypeMapper.toListResponse(foundType.getCompatibleDistributionSetTypes())); + } + + @Override + public ResponseEntity removeCompatibleDistributionSet(@PathVariable("targetTypeId") final Long targetTypeId, + @PathVariable("distributionSetTypeId") final Long distributionSetTypeId) { + + targetTypeManagement.unassignDistributionSetType(targetTypeId, distributionSetTypeId); + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity addCompatibleDistributionSets(@PathVariable("targetTypeId") final Long targetTypeId, + @RequestBody final List distributionSetTypeIds) { + + targetTypeManagement.assignCompatibleDistributionSetTypes(targetTypeId, + distributionSetTypeIds.stream().map(MgmtDistributionSetTypeAssignment::getId).collect(Collectors.toList())); + return ResponseEntity.ok().build(); + } + + private TargetType findTargetTypeWithExceptionIfNotFound(final Long targetTypeId) { + return targetTypeManagement.get(targetTypeId) + .orElseThrow(() -> new EntityNotFoundException(TargetType.class, targetTypeId)); + } + +} diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/PagingUtility.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/PagingUtility.java index b9fd6df59..305995c44 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/PagingUtility.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/PagingUtility.java @@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.SoftwareModuleTypeFields; import org.eclipse.hawkbit.repository.TagFields; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFilterQueryFields; +import org.eclipse.hawkbit.repository.TargetTypeFields; import org.eclipse.hawkbit.rest.util.SortUtility; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -61,6 +62,14 @@ public final class PagingUtility { return Sort.by(SortUtility.parse(TargetFields.class, sortParam)); } + static Sort sanitizeTargetTypeSortParam(final String sortParam) { + if (sortParam == null) { + // default + return Sort.by(Direction.ASC, TargetTypeFields.ID.getFieldName()); + } + return Sort.by(SortUtility.parse(TargetFields.class, sortParam)); + } + static Sort sanitizeTagSortParam(final String sortParam) { if (sortParam == null) { // default diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index d3aad4cae..7c60aa6b9 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -33,6 +33,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import javax.validation.ConstraintViolationException; @@ -56,6 +57,7 @@ import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetMetadata; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.exception.MessageNotReadableException; @@ -63,6 +65,7 @@ import org.eclipse.hawkbit.rest.json.model.ExceptionInfo; import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.util.IpUtil; +import org.hamcrest.Matchers; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; @@ -105,6 +108,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest private static final String JSON_PATH_FIELD_SIZE = ".size"; private static final String JSON_PATH_FIELD_TOTAL = ".total"; private static final String JSON_PATH_FIELD_LAST_REQUEST_AT = ".lastControllerRequestAt"; + private static final String JSON_PATH_FIELD_TARGET_TYPE = ".targetType"; // target // $.field @@ -117,6 +121,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest private static final String JSON_PATH_CONTROLLERID = JSON_PATH_ROOT + JSON_PATH_FIELD_CONTROLLERID; private static final String JSON_PATH_DESCRIPTION = JSON_PATH_ROOT + JSON_PATH_FIELD_DESCRIPTION; private static final String JSON_PATH_LAST_REQUEST_AT = JSON_PATH_ROOT + JSON_PATH_FIELD_LAST_REQUEST_AT; + private static final String JSON_PATH_TYPE = JSON_PATH_ROOT + JSON_PATH_FIELD_TARGET_TYPE; @Autowired private JpaProperties jpaProperties; @@ -2075,4 +2080,192 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest "$._links.rollout.href", containsString("/rest/v1/rollouts/" + rollout.getId().intValue()))); } + @Test + @Description("Ensures that a post request for creating targets with target type works.") + public void createTargetsWithTargetType() throws Exception { + final TargetType type1 = testdataFactory.createTargetType("typeWithDs", Collections.singletonList(standardDsType)); + final TargetType type2 = testdataFactory.createTargetType("typeWithOutDs", Collections.singletonList(standardDsType)); + + final Target test1 = entityFactory.target().create().controllerId("id1").name("targetWithoutType") + .securityToken("token").address("amqp://test123/foobar").description("testid1").build(); + final Target test2 = entityFactory.target().create().controllerId("id2").name("targetOfType1") + .targetType(type1.getId()).description("testid2").build(); + final Target test3 = entityFactory.target().create().controllerId("id3").name("targetOfType2") + .targetType(type2.getId()).description("testid3").build(); + final String hrefType1 = "http://localhost/rest/v1/targettypes/" + type1.getId(); + + final List targets = Arrays.asList(test1, test2, test3); + + final MvcResult mvcPostResult = mvc + .perform(post("/rest/v1/targets/").content(JsonBuilder.targets(targets, true)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("[0].name", equalTo("targetWithoutType"))) + .andExpect(jsonPath("[0].controllerId", equalTo("id1"))) + .andExpect(jsonPath("[0].description", equalTo("testid1"))) + .andExpect(jsonPath("[0].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[0].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[0].securityToken", equalTo("token"))) + .andExpect(jsonPath("[0].address", equalTo("amqp://test123/foobar"))) + .andExpect(jsonPath("[0].targetType").doesNotExist()) + .andExpect(jsonPath("[1].name", equalTo("targetOfType1"))) + .andExpect(jsonPath("[1].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[1].controllerId", equalTo("id2"))) + .andExpect(jsonPath("[1].description", equalTo("testid2"))) + .andExpect(jsonPath("[1].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[1].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[1].targetType", equalTo(type1.getId().intValue()))) + .andExpect(jsonPath("[2].name", equalTo("targetOfType2"))) + .andExpect(jsonPath("[2].controllerId", equalTo("id3"))) + .andExpect(jsonPath("[2].description", equalTo("testid3"))) + .andExpect(jsonPath("[2].createdAt", not(equalTo(0)))) + .andExpect(jsonPath("[2].createdBy", equalTo("bumlux"))) + .andExpect(jsonPath("[2].targetType", equalTo(type2.getId().intValue()))) + .andReturn(); + + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + test2.getControllerId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(JSON_PATH_NAME, equalTo("targetOfType1"))) + .andExpect(jsonPath(JSON_PATH_CONTROLLERID, equalTo("id2"))) + .andExpect(jsonPath(JSON_PATH_TYPE, equalTo(type1.getId().intValue()))) + .andExpect(jsonPath(JSON_PATH_DESCRIPTION, equalTo("testid2"))) + .andExpect(jsonPath("$._links.targetType.href", equalTo(hrefType1))).andReturn(); + + assertThat(targetManagement.getByControllerID("id1")).isNotNull(); + assertThat(targetManagement.getByControllerID("id1").get().getName()).isEqualTo("targetWithoutType"); + assertThat(targetManagement.getByControllerID("id1").get().getDescription()).isEqualTo("testid1"); + assertThat(targetManagement.getByControllerID("id1").get().getTargetType()).isNull(); + assertThat(targetManagement.getByControllerID("id1").get().getSecurityToken()).isEqualTo("token"); + assertThat(targetManagement.getByControllerID("id1").get().getAddress().toString()) + .isEqualTo("amqp://test123/foobar"); + assertThat(targetManagement.getByControllerID("id2")).isNotNull(); + assertThat(targetManagement.getByControllerID("id2").get().getName()).isEqualTo("targetOfType1"); + assertThat(targetManagement.getByControllerID("id2").get().getDescription()).isEqualTo("testid2"); + assertThat(targetManagement.getByControllerID("id2").get().getTargetType().getName()).isEqualTo("typeWithDs"); + assertThat(targetManagement.getByControllerID("id3")).isNotNull(); + assertThat(targetManagement.getByControllerID("id3").get().getName()).isEqualTo("targetOfType2"); + assertThat(targetManagement.getByControllerID("id3").get().getDescription()).isEqualTo("testid3"); + assertThat(targetManagement.getByControllerID("id3").get().getTargetType().getName()).isEqualTo("typeWithOutDs"); + } + + @Test + @Description("Ensures that a post request for creating target with target type works.") + public void createTargetWithExistingTargetType() throws Exception { + // create target type + List targetTypes = testdataFactory.createTargetTypes("targettype",1); + assertThat(targetTypes).hasSize(1); + + final Target target = entityFactory.target().create().controllerId("targetcontroller").name("testtarget").targetType(targetTypes.get(0).getId()).build(); + + final String targetList = JsonBuilder.targets(Collections.singletonList(target), false); + + // test query target over rest resource + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING).content(targetList) + .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isCreated()) + .andExpect(jsonPath("[0].controllerId", equalTo("targetcontroller"))) + .andExpect(jsonPath("[0].targetType", equalTo(targetTypes.get(0).getId().intValue()))); + + assertThat(targetManagement.getByControllerID("targetcontroller").get().getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); + } + + @Test + @Description("Ensures that a put request for updating targets with target type works.") + public void updateTargetTypeInTarget() throws Exception { + // create target type + List targetTypes = testdataFactory.createTargetTypes("targettype",2); + assertThat(targetTypes).hasSize(2); + + String controllerId = "targetcontroller"; + Target target = testdataFactory.createTarget(controllerId, "testtarget", targetTypes.get(0).getId()); + + assertThat(target).isNotNull(); + assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); + + // update target over rest resource + final String body = new JSONObject().put("targetType", targetTypes.get(1).getId().intValue()).toString(); + + mvc.perform(put(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + controllerId).content(body) + .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andExpect(jsonPath("controllerId", equalTo(controllerId))) + .andExpect(jsonPath("targetType", equalTo(targetTypes.get(1).getId().intValue()))); + } + + @Test + @Description("Ensures that a post request for creating targets with unknown target type fails.") + public void addingNonExistingTargetTypeInTargetShouldFail() throws Exception { + long unknownTargetTypeId = 999; + String errorMsg = String.format("TargetType with given identifier {%s} does not exist.", unknownTargetTypeId); + + Optional targetType = targetTypeManagement.get(unknownTargetTypeId); + assertThat(targetType).isNotPresent(); + + String controllerId = "targetcontroller"; + final Target target = entityFactory.target().create().controllerId(controllerId).name("testtarget").build(); + + final String targetList = JsonBuilder.targets(Collections.singletonList(target), false, unknownTargetTypeId); + + // post target over rest resource + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING).content(targetList) + .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isNotFound()) + .andExpect(jsonPath("message", Matchers.containsString(errorMsg))); + } + + @Test + @Description("Ensures that a post request for assign target type to target works.") + public void assignTargetTypeToTarget() throws Exception { + // create target type + TargetType targetType = testdataFactory.findOrCreateTargetType("targettype"); + assertThat(targetType).isNotNull(); + + // create target + String targetControllerId = "targetcontroller"; + Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); + assertThat(target).isNotNull(); + + // assign target type over rest resource + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetControllerId + "/targettype") + .content("{\"id\":" + targetType.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(targetManagement.getByControllerID(targetControllerId).get().getTargetType().getId()).isEqualTo(targetType.getId()); + } + + @Test + @Description("Ensures that a post request for assign a invalid target type to target fails.") + public void assignInvalidTargetTypeToTargetFails() throws Exception { + // Invalid target type ID + long invalidTargetTypeId = 999; + + // create target + String targetControllerId = "targetcontroller"; + Target target = testdataFactory.createTarget(targetControllerId, "testtarget"); + assertThat(target).isNotNull(); + + // assign invalid target type over rest resource + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetControllerId + "/targettype") + .content("{\"id\":" + invalidTargetTypeId + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + } + + @Test + @Description("Ensures that a delete request for unassign target type from target works.") + public void unassignTargetTypeFromTarget() throws Exception { + // create target type + List targetTypes = testdataFactory.createTargetTypes("targettype",1); + assertThat(targetTypes).hasSize(1); + + String targetControllerId = "targetcontroller"; + Target target = testdataFactory.createTarget(targetControllerId, "testtarget", targetTypes.get(0).getId()); + + assertThat(target).isNotNull(); + assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId()); + + // unassign target type over rest resource + mvc.perform(delete(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetControllerId + "/targettype") + .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + + assertThat(targetManagement.getByControllerID(targetControllerId).get().getTargetType()).isNull(); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResourceTest.java new file mode 100644 index 000000000..7fb7cbc93 --- /dev/null +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTypeResourceTest.java @@ -0,0 +1,616 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.rest.resource; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.jayway.jsonpath.JsonPath; +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Step; +import io.qameta.allure.Story; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.NamedEntity; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.repository.test.util.WithUser; +import org.eclipse.hawkbit.rest.util.JsonBuilder; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Spring MVC Tests against the MgmtTargetTypeResource. + * + */ +@Feature("Component Tests - Management API") +@Story("Target Type Resource") +class MgmtTargetTypeResourceTest extends AbstractManagementApiIntegrationTest { + + private final static String TARGETTYPES_ENDPOINT = MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING; + private final static String TARGETTYPE_SINGLE_ENDPOINT = MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + + "/{typeid}"; + private final static String TARGETTYPE_DSTYPES_ENDPOINT = TARGETTYPE_SINGLE_ENDPOINT + "/" + + MgmtRestConstants.TARGETTYPE_V1_DS_TYPES; + private final static String TARGETTYPE_DSTYPE_SINGLE_ENDPOINT = TARGETTYPE_DSTYPES_ENDPOINT + "/{dstypeid}"; + + private final static String TEST_USER = "targetTypeTester"; + + + @Test + @WithUser(principal = "targetTypeTester", allSpPermissions = true, removeFromAllPermission = { SpPermission.READ_TARGET }) + @Description("GET targettypes returns Forbidden when permission is missing") + void getTargetTypesWithoutPermission() throws Exception { + mvc.perform(get(TARGETTYPES_ENDPOINT).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{id} GET request.") + void getTargetType() throws Exception { + String typeName = "TestTypeGET"; + TargetType testType = createTestTargetTypeInDB(typeName); + Long typeId = testType.getId(); + + mvc.perform(get(TARGETTYPE_SINGLE_ENDPOINT, typeId).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.id", is(typeId), Long.class)).andExpect(jsonPath("$.name", equalTo(typeName))) + .andExpect(jsonPath("$.colour", is("#000000"))) + .andExpect(jsonPath("$.description", equalTo(typeName + " description"))) + .andExpect(jsonPath("$.createdBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$.lastModifiedBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.lastModifiedAt", equalTo(testType.getLastModifiedAt()))) + .andExpect(jsonPath("$._links.self.href", equalTo("http://localhost/rest/v1/targettypes/" + typeId))) + .andExpect(jsonPath("$.deleted").doesNotExist()) + .andExpect(jsonPath("$.key").doesNotExist()) + .andExpect(jsonPath("$._links.compatibledistributionsettypes.href", + equalTo("http://localhost/rest/v1/targettypes/" + typeId + "/compatibledistributionsettypes"))); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes GET requests.") + void getTargetTypes() throws Exception { + String typeName = "TestTypeGET"; + int count = 5; + List testTypes = createTestTargetTypesInDB(typeName, count); + + ResultActions resultActions = mvc.perform(get(TARGETTYPES_ENDPOINT).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()); + + for (int index = 0; index < count; index++) { + Long typeId = testTypes.get(index).getId(); + resultActions.andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].id", contains(typeId.intValue()))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].name", contains(typeName + index))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].colour", contains("#000000"))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].description", + contains(typeName + " description"))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].createdBy", contains(TEST_USER))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].createdAt", + contains(testTypes.get(index).getCreatedAt()))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].lastModifiedBy", contains(TEST_USER))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].lastModifiedAt", + contains(testTypes.get(index).getLastModifiedAt()))) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].deleted").doesNotExist()) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')].key").doesNotExist()) + .andExpect(jsonPath("$.content.[?(@.id=='" + typeId + "')]._links.self.href", + contains("http://localhost/rest/v1/targettypes/" + typeId))) + .andExpect( + jsonPath("$.content.[?(@.id=='" + typeId + "')]._links.compatibledistributionsettypes.href") + .doesNotExist()) + .andExpect(jsonPath("$.total", equalTo(count))).andExpect(jsonPath("$.size", equalTo(count))); + } + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes GET requests without prior created target types.") + void getDefaultTargetTypes() throws Exception { + + // 0 types overall (no default types are created) + final int types = 0; + mvc.perform(get(TARGETTYPES_ENDPOINT)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(types))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(types))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(types))); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes GET requests with sorting by name.") + void getTargetTypesSortedByName() throws Exception { + String typeNameA = "ATestTypeGETsorted"; + String typeNameB = "BTestTypeGETsorted"; + String typeNameC = "CTestTypeGETsorted"; + TargetType testTypeB = createTestTargetTypeInDB(typeNameB); + TargetType testTypeC = createTestTargetTypeInDB(typeNameC); + TargetType testTypeA = createTestTargetTypeInDB(typeNameA); + + testTypeA = targetTypeManagement + .update(entityFactory.targetType().update(testTypeA.getId()).description("Updated description")); + + // descending + mvc.perform(get(TARGETTYPES_ENDPOINT).accept(MediaType.APPLICATION_JSON) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "name:DESC")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.content.[0].id", equalTo(testTypeC.getId().intValue()))) + .andExpect(jsonPath("$.content.[0].name", equalTo(typeNameC))) + .andExpect(jsonPath("$.content.[0].colour", equalTo("#000000"))) + .andExpect(jsonPath("$.content.[0].description", equalTo(typeNameC + " description"))) + .andExpect(jsonPath("$.content.[0].createdBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.content.[0].createdAt", equalTo(testTypeC.getCreatedAt()))) + .andExpect(jsonPath("$.content.[0].lastModifiedBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.content.[0].lastModifiedAt", equalTo(testTypeC.getLastModifiedAt()))) + .andExpect(jsonPath("$.content.[0].deleted").doesNotExist()) + .andExpect(jsonPath("$.content.[0].key").doesNotExist()) + .andExpect(jsonPath("$.content.[0]._links.self.href", + equalTo("http://localhost/rest/v1/targettypes/" + testTypeC.getId()))) + .andExpect(jsonPath("$.content.[0]._links.compatibledistributionsettypes.href").doesNotExist()) + .andExpect(jsonPath("$.total", equalTo(3))).andExpect(jsonPath("$.size", equalTo(3))) + .andExpect(jsonPath("$.content.[1].name", equalTo(typeNameB))) + .andExpect(jsonPath("$.content.[2].name", equalTo(typeNameA))); + + // ascending + mvc.perform(get(TARGETTYPES_ENDPOINT).accept(MediaType.APPLICATION_JSON) + .param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "name:ASC")).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.content.[0].id", equalTo(testTypeA.getId().intValue()))) + .andExpect(jsonPath("$.content.[0].name", equalTo(typeNameA))) + .andExpect(jsonPath("$.content.[0].description", equalTo("Updated description"))) + .andExpect(jsonPath("$.content.[0].colour", equalTo("#000000"))) + .andExpect(jsonPath("$.content.[0].createdBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.content.[0].createdAt", equalTo(testTypeA.getCreatedAt()))) + .andExpect(jsonPath("$.content.[0].lastModifiedBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.content.[0].lastModifiedAt", equalTo(testTypeA.getLastModifiedAt()))) + .andExpect(jsonPath("$.content.[0].deleted").doesNotExist()) + .andExpect(jsonPath("$.content.[0].key").doesNotExist()) + .andExpect(jsonPath("$.content.[0]._links.self.href", + equalTo("http://localhost/rest/v1/targettypes/" + testTypeA.getId()))) + .andExpect(jsonPath("$.content.[0]._links.compatibledistributionsettypes.href").doesNotExist()) + .andExpect(jsonPath("$.total", equalTo(3))).andExpect(jsonPath("$.size", equalTo(3))) + .andExpect(jsonPath("$.content.[1].name", equalTo(typeNameB))) + .andExpect(jsonPath("$.content.[2].name", equalTo(typeNameC))); + + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes GET requests with paging.") + void getTargetTypesWithPagingLimitRequestParameter() throws Exception { + final String typePrefix = "TestTypeGETPaging"; + final int count = 10; + final int limit = 3; + createTestTargetTypesInDB(typePrefix, count); + + mvc.perform(get(TARGETTYPES_ENDPOINT).param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, + String.valueOf(limit))).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(count))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(limit))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(limit))); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes GET requests with paging and offset.") + void getTargetTypesWithPagingLimitAndOffsetRequestParameter() throws Exception { + final int count = 10; + final int offset = 2; + final int expectedSize = count - offset; + final String typePrefix = "TestTypeGETPaging"; + createTestTargetTypesInDB(typePrefix, count); + + mvc.perform(get(TARGETTYPES_ENDPOINT) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offset)) + .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(count))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_TOTAL, equalTo(count))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) + .andExpect(jsonPath(MgmtTargetResourceTest.JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID} PUT requests.") + void updateTargetType() throws Exception { + String typeName = "TestTypePUT"; + final TargetType testType = createTestTargetTypeInDB(typeName); + final String body = new JSONObject().put("id", testType.getId()).put("description", "updated description") + .put("name", "TestTypePUTupdated").put("colour", "#ffffff").toString(); + + mvc.perform( + put(TARGETTYPE_SINGLE_ENDPOINT, testType.getId()).content(body).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$.description", equalTo("updated description"))) + .andExpect(jsonPath("$.name", equalTo("TestTypePUTupdated"))) + .andExpect(jsonPath("$.colour", equalTo("#ffffff"))).andReturn(); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{id} GET requests.") + void getUpdatedTargetType() throws Exception { + TargetType testType = createTestTargetTypeInDB("TestTypeGET"); + String typeNameUpdated = "TestTypeGETupdated"; + testType = targetTypeManagement.update(entityFactory.targetType().update(testType.getId()).name(typeNameUpdated) + .description("Updated Description").colour("#ffffff")); + + mvc.perform(get(TARGETTYPE_SINGLE_ENDPOINT, testType.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.name", equalTo(typeNameUpdated))) + .andExpect(jsonPath("$.description", equalTo("Updated Description"))) + .andExpect(jsonPath("$.colour", equalTo("#ffffff"))) + .andExpect(jsonPath("$.createdBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.createdAt", equalTo(testType.getCreatedAt()))) + .andExpect(jsonPath("$.lastModifiedBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$.lastModifiedAt", equalTo(testType.getLastModifiedAt()))) + .andExpect(jsonPath("$.deleted").doesNotExist()) + .andExpect(jsonPath("$.key").doesNotExist()); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes POST requests.") + void createTargetTypes() throws Exception { + String typeName = "TestTypePOST"; + final List types = buildTestTargetTypesWithoutDsTypes(typeName, 5); + + runPostTargetTypeAndVerify(types); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID}/compatibledistributionsettypes POST requests.") + void addDistributionSetTypeToTargetType() throws Exception { + String typeName = "TestTypeAddDs"; + TargetType testType = createTestTargetTypeInDB(typeName); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + + mvc.perform(post(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId()) + .content("[{\"id\":" + standardDsType.getId() + "}]").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + testType = targetTypeManagement.get(testType.getId()).get(); + assertThat(testType.getLastModifiedBy()).isEqualTo(TEST_USER); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getCompatibleDistributionSetTypes()).containsExactly(standardDsType); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID}/compatibledistributionsettypes GET requests.") + void getDistributionSetsOfTargetType() throws Exception { + String typeName = "TestTypeGetDs"; + final TargetType testType = createTestTargetTypeInDB(typeName, Collections.singletonList(standardDsType)); + + mvc.perform(get(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$[0].name", equalTo(standardDsType.getName()))) + .andExpect(jsonPath("$[0].description", equalTo(standardDsType.getDescription()))) + .andExpect(jsonPath("$[0].key", equalTo("test_default_ds_type"))) + .andExpect(jsonPath("$[0]._links.self.href", + equalTo("http://localhost/rest/v1/distributionsettypes/" + standardDsType.getId()))); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID}/compatibledistributionsettypes/{ID} GET requests.") + void getDistributionSetOfTargetTypeReturnsNotAllowed() throws Exception { + String typeName = "TestTypeAddDs"; + final TargetType testType = createTestTargetTypeInDB(typeName); + + mvc.perform(get(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), standardDsType.getId()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID}/compatibledistributionsettypes/{ID} DELETE requests.") + void removeDsTypeFromTargetType() throws Exception { + String typeName = "TestTypeRemoveDs"; + TargetType testType = createTestTargetTypeInDB(typeName, Collections.singletonList(standardDsType)); + + mvc.perform(delete(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), standardDsType.getId()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + testType = targetTypeManagement.get(testType.getId()).get(); + assertThat(testType.getLastModifiedBy()).isEqualTo(TEST_USER); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getCompatibleDistributionSetTypes()).isEmpty(); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID} DELETE requests.") + void deletingDsTypeRemovesAssignmentFromTargetType() throws Exception { + String typeName = "TestTypeRemoveDs"; + TargetType testType = createTestTargetTypeInDB(typeName, Collections.singletonList(standardDsType)); + assertThat(testType.getCompatibleDistributionSetTypes()).hasSize(1); + assertThat(distributionSetTypeManagement.getByKey(standardDsType.getKey())).isNotEmpty(); + + mvc.perform(delete(MgmtRestConstants.DISTRIBUTIONSETTYPE_V1_REQUEST_MAPPING + "/" + standardDsType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + testType = targetTypeManagement.get(testType.getId()).get(); + assertThat(testType.getLastModifiedBy()).isEqualTo(TEST_USER); + assertThat(testType.getOptLockRevision()).isEqualTo(2); + assertThat(testType.getCompatibleDistributionSetTypes()).isEmpty(); + assertThat(distributionSetTypeManagement.getByKey(standardDsType.getKey())).isEmpty(); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID} DELETE requests - Deletion when not in use.") + void deleteTargetTypeUnused() throws Exception { + String typeName = "TestTypeUnusedDelete"; + final TargetType testType = createTestTargetTypeInDB(typeName); + + assertThat(targetTypeManagement.count()).isEqualTo(1); + + mvc.perform(delete(TARGETTYPE_SINGLE_ENDPOINT, testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + assertThat(targetTypeManagement.count()).isZero(); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Checks the correct behaviour of /rest/v1/targettypes/{ID} DELETE requests - Deletion not possible when in use.") + void deleteTargetTypeUsed() throws Exception { + String typeName = "TestTypeUsedDelete"; + final TargetType testType = createTestTargetTypeInDB(typeName); + + targetManagement.create(entityFactory.target().create().controllerId("target").name("TargetOfTestType") + .description("target description").targetType(testType.getId())); + + assertThat(targetTypeManagement.count()).isEqualTo(1); + assertThat(targetManagement.count()).isEqualTo(1); + + mvc.perform(delete(TARGETTYPE_SINGLE_ENDPOINT, testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isConflict()); + + assertThat(targetManagement.count()).isEqualTo(1); + assertThat(targetTypeManagement.count()).isEqualTo(1); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Ensures that target type deletion request to API on an entity that does not exist results in NOT_FOUND.") + void deleteTargetTypeThatDoesNotExistLeadsToNotFound() throws Exception { + mvc.perform(delete(TARGETTYPE_SINGLE_ENDPOINT, 1234)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + } + + @Test + @WithUser(principal = TEST_USER, allSpPermissions = true) + @Description("Tests the update of the deletion flag. It is verified that the target type can't be marked as deleted through update operation.") + void updateTargetTypeDeletedFlag() throws Exception { + String typeName = "TestTypePUT"; + final TargetType testType = createTestTargetTypeInDB(typeName); + + final String body = new JSONObject().put("id", testType.getId()).put("deleted", true).toString(); + + mvc.perform( + put(TARGETTYPE_SINGLE_ENDPOINT, testType.getId()).content(body).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.id", equalTo(testType.getId().intValue()))) + .andExpect(jsonPath("$.deleted").doesNotExist()); + } + + @Test + @Description("Ensures that the server is behaving as expected on invalid requests (wrong media type, wrong ID etc.).") + void invalidRequestsOnTargetTypesResource() throws Exception { + String typeName = "TestTypeInvalidReq"; + final TargetType testType = createTestTargetTypeInDB(typeName, Collections.singletonList(standardDsType)); + + // target type does not exist + mvc.perform(get(TARGETTYPE_SINGLE_ENDPOINT, 12345678)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + mvc.perform(get(TARGETTYPE_DSTYPES_ENDPOINT, 123456789)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + mvc.perform(delete(TARGETTYPE_SINGLE_ENDPOINT, 123456789)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // target types at creation time invalid + final TargetType testNewType = createTestTargetTypeInDB(typeName + "Another", + Collections.singletonList(standardDsType)); + + mvc.perform(post(TARGETTYPES_ENDPOINT).content(JsonBuilder.targetTypes(Collections.singletonList(testNewType))) + .contentType(MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isUnsupportedMediaType()); + + // bad request - no content + mvc.perform(post(TARGETTYPES_ENDPOINT).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + + // bad request - bad content + mvc.perform(post(TARGETTYPES_ENDPOINT).content("sdfjsdlkjfskdjf".getBytes()) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // Missing mandatory field name + mvc.perform(post(TARGETTYPES_ENDPOINT).content("[{\"description\":\"Desc123\"}]") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + final TargetType tooLongName = entityFactory.targetType().create() + .name(RandomStringUtils.randomAlphanumeric(NamedEntity.NAME_MAX_SIZE + 1)).build(); + mvc.perform(post(TARGETTYPES_ENDPOINT).content(JsonBuilder.targetTypes(Collections.singletonList(tooLongName))) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + // ds types + mvc.perform(get(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + mvc.perform(delete(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), 565765)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); + + mvc.perform(post(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId()).content("{\"id\":1}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + + mvc.perform(post(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId()).content("[{\"id\":44456}]") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isNotFound()); + + // not allowed methods + mvc.perform(put(TARGETTYPES_ENDPOINT)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete(TARGETTYPES_ENDPOINT)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId())).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(delete(TARGETTYPE_DSTYPES_ENDPOINT, testType.getId(), 565765)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(put(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), 565765)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + + mvc.perform(post(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), 565765) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isMethodNotAllowed()); + + mvc.perform(get(TARGETTYPE_DSTYPE_SINGLE_ENDPOINT, testType.getId(), 565765)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed()); + } + + @Test + @Description("Search request of target types.") + void searchTargetTypeRsql() throws Exception { + targetTypeManagement.create(entityFactory.targetType().create().name("TestName123")); + targetTypeManagement.create(entityFactory.targetType().create().name("TestName1234")); + + final String rsqlFindLikeDs1OrDs2 = "name==TestName123,name==TestName1234"; + + mvc.perform(get(TARGETTYPES_ENDPOINT + "?q=" + rsqlFindLikeDs1OrDs2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("size", equalTo(2))) + .andExpect(jsonPath("total", equalTo(2))).andExpect(jsonPath("content[0].name", equalTo("TestName123"))) + .andExpect(jsonPath("content[1].name", equalTo("TestName1234"))); + } + + @Step + private TargetType buildTestTargetTypeBody(String name) { + return prepareTestTargetType(name, null).build(); + } + + private TargetTypeCreate prepareTestTargetType(String name, Collection dsTypes) { + TargetTypeCreate create = entityFactory.targetType().create().name(name) + .description("Description of the test type").colour("#aaaaaa"); + if (dsTypes != null && !dsTypes.isEmpty()) { + create.compatible(Collections.singletonList(standardDsType.getId())); + } + return create; + } + + @Step + private List createTestTargetTypesInDB(String namePrefix, int count) { + return testdataFactory.createTargetTypes(namePrefix, count); + } + + @Step + private TargetType createTestTargetTypeInDB(String name) { + return testdataFactory.findOrCreateTargetType(name); + } + + @Step + private TargetType createTestTargetTypeInDB(String name, List dsTypes) { + TargetType targetType = testdataFactory.createTargetType(name, dsTypes); + assertThat(targetType.getOptLockRevision()).isEqualTo(1); + return targetType; + } + + @Step + private List buildTestTargetTypesWithoutDsTypes(String namePrefix, int count) { + final List types = new ArrayList<>(); + for (int index = 0; index < count; index++) { + types.add(buildTestTargetTypeBody(namePrefix + index)); + } + return types; + } + + @Step + private void runPostTargetTypeAndVerify(final List types) throws Exception { + int size = types.size(); + ResultActions resultActions = mvc + .perform(post(TARGETTYPES_ENDPOINT).content(JsonBuilder.targetTypes(types)) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()); + + for (int index = 0; index < size; index++) { + resultActions.andExpect(status().isCreated()).andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$[" + index + "].id").exists()) + .andExpect(jsonPath("$[" + index + "].name", startsWith("TestTypePOST"))) + .andExpect(jsonPath("$[" + index + "].colour", hasToString("#aaaaaa"))) + .andExpect(jsonPath("$[" + index + "].description", + equalTo("Description of the test type"))) + .andExpect(jsonPath("$[" + index + "].createdBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$[" + index + "].createdAt").exists()) + .andExpect(jsonPath("$[" + index + "].lastModifiedBy", equalTo(TEST_USER))) + .andExpect(jsonPath("$[" + index + "].lastModifiedAt").exists()) + .andExpect(jsonPath("$[" + index + "].key").doesNotExist()) + .andExpect(jsonPath("$[" + index + "].deleted").doesNotExist()) + .andExpect(jsonPath("$[" + index + "]._links.self.href", + startsWith("http://localhost/rest/v1/targettypes/"))) + .andExpect( + jsonPath("$[" + index + "]._links.compatibledistributionsettypes.href") + .doesNotExist()); + } + MvcResult mvcResult = resultActions.andReturn(); + + for (int index = 0; index < size; index++) { + String name = "TestTypePOST" + index; + final TargetType created = targetTypeManagement.getByName(name).get(); + + assertThat(JsonPath.compile("$[ ?(@.name=='" + name + "') ].id") + .read(mvcResult.getResponse().getContentAsString()).toString()).contains(String.valueOf(created.getId())); + assertThat(JsonPath.compile("$[ ?(@.name=='" + name + "') ]._links.self.href") + .read(mvcResult.getResponse().getContentAsString()).toString()).contains("/"+created.getId()); + } + + assertThat(targetTypeManagement.count()).isEqualTo(size); + } + +} diff --git a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java index 283c7f59a..2480a2118 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java +++ b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java @@ -87,6 +87,7 @@ public class ResponseExceptionHandler { ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_IN_USE, HttpStatus.CONFLICT); } private static HttpStatus getStatusOrDefault(final SpServerError error) { diff --git a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 2c537525d..1046a7346 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -23,6 +23,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetType; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -426,13 +427,13 @@ public abstract class JsonBuilder { int i = 0; for (final Target target : targets) { final String address = target.getAddress() != null ? target.getAddress().toString() : null; - + final String targetType = target.getTargetType() != null ? target.getTargetType().getId().toString() : null; final String token = withToken ? target.getSecurityToken() : null; builder.append(new JSONObject().put("controllerId", target.getControllerId()) .put("description", target.getDescription()).put("name", target.getName()).put("createdAt", "0") - .put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") - .put("address", address).put("securityToken", token).toString()); + .put("updatedAt", "0").put("createdBy", "systemtest").put("updatedBy", "systemtest") + .put("address", address).put("securityToken", token).put("targetType", targetType).toString()); if (++i < targets.size()) { builder.append(","); @@ -444,6 +445,83 @@ public abstract class JsonBuilder { return builder.toString(); } + public static String targets(final List targets, final boolean withToken, final long targetTypeId) throws JSONException { + final StringBuilder builder = new StringBuilder(); + + builder.append("["); + int i = 0; + for (final Target target : targets) { + final String address = target.getAddress() != null ? target.getAddress().toString() : null; + final String type = target.getTargetType() != null ? target.getTargetType().getId().toString() : null; + final String token = withToken ? target.getSecurityToken() : null; + + builder.append(new JSONObject().put("controllerId", target.getControllerId()) + .put("description", target.getDescription()).put("name", target.getName()).put("createdAt", "0") + .put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") + .put("address", address).put("securityToken", token).put("targetType", targetTypeId).toString()); + + if (++i < targets.size()) { + builder.append(","); + } + } + + builder.append("]"); + + return builder.toString(); + } + + public static String targetTypes(final List types) throws JSONException { + final JSONArray result = new JSONArray(); + + for (final TargetType type : types) { + + final JSONArray dsTypes = new JSONArray(); + type.getCompatibleDistributionSetTypes().forEach(dsType -> { + try { + dsTypes.put(new JSONObject().put("id", dsType.getId())); + } catch (final JSONException e1) { + e1.printStackTrace(); + } + }); + + result.put(new JSONObject().put("name", type.getName()).put("description", type.getDescription()) + .put("id", Long.MAX_VALUE).put("colour", type.getColour()).put("createdAt", "0").put("updatedAt", "0") + .put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") + .put("distributionsets", dsTypes)); + + } + + return result.toString(); + } + + public static String targetTypesCreatableFieldsOnly(final List types) throws JSONException { + final JSONArray result = new JSONArray(); + + for (final TargetType type : types) { + + final JSONArray dsTypes = new JSONArray(); + type.getCompatibleDistributionSetTypes().forEach(dsType -> { + try { + dsTypes.put(new JSONObject().put("id", dsType.getId())); + } catch (final JSONException e1) { + e1.printStackTrace(); + } + }); + + JSONObject json = new JSONObject().put("name", type.getName()).put("description", type.getDescription()) + .put("colour", type.getColour()); + + if(dsTypes.length() != 0) + { + json.put("compatibledistributionsettypes", dsTypes); + } + + result.put(json); + } + + return result.toString(); + } + public static String rollout(final String name, final String description, final int groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null, diff --git a/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targettypes-api-guide.adoc b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targettypes-api-guide.adoc new file mode 100644 index 000000000..9534c8fde --- /dev/null +++ b/hawkbit-rest/hawkbit-rest-docs/src/main/asciidoc/targettypes-api-guide.adoc @@ -0,0 +1,396 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: macro +:toclevels: 1 +:sectlinks: +:linkattrs: + +[[target-types]] += Target Types + +toc::[] + + +== GET /rest/v1/targettypes + +=== Implementation notes + +Handles the GET request of retrieving all target types within Hawkbit. Required Permission: READ_TARGET + +=== Get target types + +==== CURL + +include::{snippets}/targettypes/get-target-types/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/get-target-types/http-request.adoc[] + +==== Request query parameter + +include::{snippets}/targettypes/get-target-types-with-parameters/request-parameters.adoc[] + +==== Request parameter example + +include::{snippets}/targettypes/get-target-types-with-parameters/http-request.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targettypes/get-target-types/response-fields.adoc[] + +==== Response example + +include::{snippets}/targettypes/get-target-types/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + + +== POST /rest/v1/targettypes + +=== Implementation notes + +Handles the POST request for creating new target types within Hawkbit. The request body must always be a list of types. Required Permission: CREATE_TARGET + +=== Create target types + +==== CURL + +include::{snippets}/targettypes/post-target-types/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/post-target-types/http-request.adoc[] + +==== Request fields + +include::{snippets}/targettypes/post-target-types/request-fields.adoc[] + +=== Response (Status 201) + +==== Response fields + +include::{snippets}/targettypes/post-target-types/response-fields.adoc[] + +==== Response example + +include::{snippets}/targettypes/post-target-types/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Target type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +| `409 Conflict` +| Target type already exists +| See <> +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + +== DELETE /rest/v1/targettypes/{targetTypeId} + +=== Implementation Notes + +Handles the DELETE request for a single target type within Hawkbit. Required Permission: DELETE_TARGET + +=== Delete target type + +==== CURL + +include::{snippets}/targettypes/delete-target-type/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/delete-target-type/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/delete-target-type/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response example + +include::{snippets}/targettypes/delete-target-type/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Target type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + + +== GET /rest/v1/targettypes/{targetTypeId} + +=== Implementation notes + +Handles the GET request of retrieving a single target type within Hawkbit. Required Permission: READ_TARGET + +=== Get target type + +==== CURL + +include::{snippets}/targettypes/get-target-type/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/get-target-type/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/get-target-type/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targettypes/get-target-type/response-fields.adoc[] + +==== Response example + +include::{snippets}/targettypes/get-target-type/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Target type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + + +== PUT /rest/v1/targettypes/{targetTypeId} + +=== Implementation notes + +Handles the PUT request for a single target type within Hawkbit. Required Permission: UPDATE_TARGET + +=== Update target type + +==== CURL + +include::{snippets}/targettypes/put-target-type/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/put-target-type/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/put-target-type/path-parameters.adoc[] + +==== Request fields + +include::{snippets}/targettypes/put-target-type/request-fields.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targettypes/put-target-type/response-fields.adoc[] + +==== Response example + +include::{snippets}/targettypes/put-target-type/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Target type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/409.adoc[] +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + +== GET /rest/v1/targettypes/{targetTypeId}/compatibledistributionsettypes + +=== Implementation notes + +Handles the GET request of retrieving the list of compatible distribution set types in that target type. Required Permission: READ_TARGET, READ_REPOSITORY + +=== Lists all compatible distribution set types + +==== CURL + +include::{snippets}/targettypes/get-compatible-distribution-set-types/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/get-compatible-distribution-set-types/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/get-compatible-distribution-set-types/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response fields + +include::{snippets}/targettypes/get-compatible-distribution-set-types/response-fields.adoc[] + +==== Response example + +include::{snippets}/targettypes/get-compatible-distribution-set-types/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Distribution set type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + + +== POST /rest/v1/targettypes/{targetTypeId}/compatibledistributionsettypes + +=== Implementation notes + +Handles the POST request for adding compatible distribution set types to a target type. Required Permission: UPDATE_TARGET and READ_REPOSITORY + +=== Add compatible distribution set type + +==== CURL + +include::{snippets}/targettypes/post-compatible-distribution-set-types/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/post-compatible-distribution-set-types/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/post-compatible-distribution-set-types/path-parameters.adoc[] + +==== Request fields + +include::{snippets}/targettypes/post-compatible-distribution-set-types/request-fields.adoc[] + +=== Response (Status 201) + +==== Response example + +include::{snippets}/targettypes/post-compatible-distribution-set-types/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Distribution set type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +| `409 Conflict` +| Distribution set type already exists +| See <> +include::../errors/415.adoc[] +include::../errors/429.adoc[] +|=== + + +== DELETE /rest/v1/targettypes/{targetTypeId}/compatibledistributionsettypes/{distributionSetTypeId} + +=== Implementation Notes + +Handles the DELETE request for removing a distribution set type from a single target type. Required Permission: UPDATE_TARGET and READ_REPOSITORY + +=== Remove compatible distribution set type from target type + +==== CURL + +include::{snippets}/targettypes/delete-compatible-distribution-set-type/curl-request.adoc[] + +==== Request URL + +include::{snippets}/targettypes/delete-compatible-distribution-set-type/http-request.adoc[] + +==== Request path parameter + +include::{snippets}/targettypes/delete-compatible-distribution-set-type/path-parameters.adoc[] + +=== Response (Status 200) + +==== Response example + +include::{snippets}/targettypes/delete-compatible-distribution-set-type/http-response.adoc[] + +=== Error responses + +|=== +| HTTP Status Code | Reason | Response Model + +include::../errors/400.adoc[] +include::../errors/401.adoc[] +include::../errors/403.adoc[] +| `404 Not Found` +| Distribution set type was not found. +| See <> +include::../errors/405.adoc[] +include::../errors/406.adoc[] +include::../errors/429.adoc[] +|=== + diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/ApiModelPropertiesGeneric.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/ApiModelPropertiesGeneric.java index 1dfefb9d6..869f4af96 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/ApiModelPropertiesGeneric.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/ApiModelPropertiesGeneric.java @@ -19,12 +19,12 @@ public final class ApiModelPropertiesGeneric { public static final String ITEM_ID = "The technical identifier " + ENDING; public static final String NAME = "The name" + ENDING; public static final String DESCRPTION = "The description" + ENDING; - public static final String COLOUR = "The colour" + ENDING; + public static final String COLOUR = "The colour" + ENDING + ". In HEX format, e.g. #800080"; public static final String DELETED = "Deleted flag, used for soft deleted entities"; - public static final String CREATED_BY = "Entity was originally created by User, AMQP-Controller, anonymous etc.)"; + public static final String CREATED_BY = "Entity was originally created by (User, AMQP-Controller, anonymous etc.)"; public static final String CREATED_AT = "Entity was originally created at (timestamp UTC in milliseconds)"; - public static final String LAST_MODIFIED_BY = "Entity was last modified by User, AMQP-Controller, anonymous etc.)"; + public static final String LAST_MODIFIED_BY = "Entity was last modified by (User, AMQP-Controller, anonymous etc.)"; public static final String LAST_MODIFIED_AT = "Entity was last modified at (timestamp UTC in milliseconds)"; // Paging elements diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 43e1a2abf..188cd4b84 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -78,6 +78,10 @@ public final class MgmtApiModelProperties { public static final String POLL_STATUS = "Poll status of the target. In many scenarios that target will poll the update server on a regular basis to look for potential updates. If that poll does not happen it might imply that the target is offline."; public static final String POLL_OVERDUE = "Defines if the target poll time is overdue based on the next expected poll time plus the configured overdue poll time threshold."; + // Target type + public static final String COMPATIBLE_DS_TYPES = "Array of distribution set types that are compatible to that target type"; + public static final String LINK_COMPATIBLE_DS_TYPES = "Link to the compatible distribution set types in this target type"; + // rollout public static final String ROLLOUT_FILTER_QUERY = "target filter query language expression"; public static final String ROLLOUT_GROUP_FILTER_QUERY = "target filter query language expression that selects a subset of targets which match the target filter of the Rollout"; @@ -118,6 +122,8 @@ public final class MgmtApiModelProperties { public static final String TARGET_LIST = "List of provisioning targets."; + public static final String TARGET_TYPE_LIST = "List of target types"; + public static final String SM_LIST = "List of software modules."; public static final String ROLLOUT_LIST = "list of rollouts"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetTypesDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetTypesDocumentationTest.java new file mode 100644 index 000000000..053ed29b3 --- /dev/null +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetTypesDocumentationTest.java @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.rest.mgmt.documentation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.rest.documentation.AbstractApiRestDocumentation; +import org.eclipse.hawkbit.rest.documentation.ApiModelPropertiesGeneric; +import org.eclipse.hawkbit.rest.documentation.MgmtApiModelProperties; +import org.eclipse.hawkbit.rest.util.JsonBuilder; +import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Documentation generation for Management API for {@link TargetType}. + * + */ +@Feature("Spring Rest Docs Tests - TargetType") +@Story("TargetTypes Resource") +public class TargetTypesDocumentationTest extends AbstractApiRestDocumentation { + @Override + public String getResourceName() { + return "targettypes"; + } + + @Test + @Description("Handles the GET request of retrieving all target types within Hawkbit. Required Permission: " + + SpPermission.READ_TARGET) + public void getTargetTypes() throws Exception { + testdataFactory.findOrCreateTargetType("targetType1"); + testdataFactory.createTargetType("targetType2", Collections.singletonList(standardDsType)); + + mockMvc.perform(get(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document(responseFields( + fieldWithPath("size").type(JsonFieldType.NUMBER).description(ApiModelPropertiesGeneric.SIZE), + fieldWithPath("total").description(ApiModelPropertiesGeneric.TOTAL_ELEMENTS), + fieldWithPath("content").description(MgmtApiModelProperties.TARGET_TYPE_LIST), + fieldWithPath("content[].id").description(ApiModelPropertiesGeneric.ITEM_ID), + fieldWithPath("content[].name").description(ApiModelPropertiesGeneric.NAME), + fieldWithPath("content[].description").description(ApiModelPropertiesGeneric.DESCRPTION), + fieldWithPath("content[].colour").description(ApiModelPropertiesGeneric.COLOUR), + fieldWithPath("content[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("content[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("content[].lastModifiedAt") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), + fieldWithPath("content[].lastModifiedBy") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"), + fieldWithPath("content[]._links.self").ignored()))); + } + + @Test + @Description("Handles the GET request of retrieving all target types within Hawkbit with a defined page size and " + + "offset, sorted by name in descending order and filtered down to all targets which name starts with 'targetType'. " + + "Required Permission: " + SpPermission.READ_TARGET) + public void getTargetTypesWithParameters() throws Exception { + testdataFactory.findOrCreateTargetType("targetType1"); + testdataFactory.createTargetType("targetType2", Collections.singletonList(standardDsType)); + + mockMvc.perform(get(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING).accept(MediaType.APPLICATION_JSON) + .param("offset", "0").param("limit", "2").param("sort", "name:ASC").param("q", "name==targetType*")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andDo(this.document.document(getFilterRequestParamter())); + } + + @Test + @Description("Handles the GET request for a single target type within Hawkbit. Required Permission: " + + SpPermission.READ_TARGET) + public void getTargetType() throws Exception { + TargetType testType = testdataFactory.createTargetType("TargetType", Collections.singletonList(standardDsType)); + + mockMvc.perform(get(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + "/{targetTypeId}", testType.getId()) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)), + responseFields(fieldWithPath("id").description(ApiModelPropertiesGeneric.ITEM_ID), + fieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + fieldWithPath("description").description(ApiModelPropertiesGeneric.DESCRPTION), + fieldWithPath("colour").description(ApiModelPropertiesGeneric.COLOUR), + fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT) + .type("Number"), + fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY) + .type("String"), + fieldWithPath("_links.compatibledistributionsettypes.href") + .description(MgmtApiModelProperties.LINK_COMPATIBLE_DS_TYPES), + fieldWithPath("_links.self").ignored()))); + } + + @Test + @Description("Handles the POST request for creating new target types within Hawkbit. The request body " + + "must always be a list of types. Required Permission: " + SpPermission.CREATE_TARGET) + public void postTargetTypes() throws Exception { + final List types = new ArrayList<>(); + types.add(entityFactory.targetType().create().name("targetType1").description("targetType1 description") + .colour("#ffffff").build()); + types.add(entityFactory.targetType().create().name("targetType2").description("targetType2 description") + .colour("#000000").compatible(Collections.singletonList(standardDsType.getId())).build()); + + mockMvc.perform(post(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING) + .content(JsonBuilder.targetTypesCreatableFieldsOnly(types)).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) + .andDo(this.document.document( + requestFields(requestFieldWithPath("[]name").description(ApiModelPropertiesGeneric.NAME), + optionalRequestFieldWithPath("[]description") + .description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("[]colour").description(ApiModelPropertiesGeneric.COLOUR), + optionalRequestFieldWithPath("[]compatibledistributionsettypes") + .description(MgmtApiModelProperties.COMPATIBLE_DS_TYPES)), + responseFields(fieldWithPath("[]id").description(ApiModelPropertiesGeneric.ITEM_ID), + fieldWithPath("[]name").description(ApiModelPropertiesGeneric.NAME), + fieldWithPath("[]description").description(ApiModelPropertiesGeneric.DESCRPTION), + fieldWithPath("[]colour").description(ApiModelPropertiesGeneric.COLOUR), + fieldWithPath("[]createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("[]createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("[]lastModifiedAt") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), + fieldWithPath("[]lastModifiedBy") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"), + fieldWithPath("[]_links.self").ignored()))); + } + + @Test + @Description("Handles the DELETE request of retrieving a single target type within Hawkbit. Required Permission: " + + SpPermission.DELETE_TARGET) + public void deleteTargetType() throws Exception { + TargetType testType = testdataFactory.createTargetType("TargetType", Collections.singletonList(standardDsType)); + + mockMvc.perform(delete(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + "/{targetTypeId}", testType.getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document(pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)))); + } + + @Test + @Description("Handles the PUT request for a single target type within Hawkbit. " + "Required Permission: " + + SpPermission.UPDATE_TARGET) + public void putTargetType() throws Exception { + TargetType testType = testdataFactory.createTargetType("targetType", Collections.singletonList(standardDsType)); + final String body = new JSONObject().put("description", "an updated description").put("name", "updatedTypeName") + .put("colour", "#aaafff").toString(); + + this.mockMvc + .perform(put(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + "/{targetTypeId}", testType.getId()) + .content(body).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)), + requestFields( + optionalRequestFieldWithPath("description") + .description(ApiModelPropertiesGeneric.DESCRPTION), + optionalRequestFieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + optionalRequestFieldWithPath("colour").description(ApiModelPropertiesGeneric.COLOUR)), + responseFields(fieldWithPath("id").description(ApiModelPropertiesGeneric.ITEM_ID), + fieldWithPath("name").description(ApiModelPropertiesGeneric.NAME), + fieldWithPath("description").description(ApiModelPropertiesGeneric.DESCRPTION), + fieldWithPath("colour").description(ApiModelPropertiesGeneric.COLOUR), + fieldWithPath("createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT) + .type("Number"), + fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY) + .type("String"), + fieldWithPath("_links.compatibledistributionsettypes.href") + .description(MgmtApiModelProperties.LINK_COMPATIBLE_DS_TYPES), + fieldWithPath("_links.self").ignored()))); + } + + @Test + @Description("Handles the GET request of retrieving the list of compatible distribution set types in that target type. " + + "Required Permission: " + SpPermission.READ_TARGET + " and " + SpPermission.READ_REPOSITORY) + public void getCompatibleDistributionSetTypes() throws Exception { + TargetType testType = testdataFactory.createTargetType("targetType", Collections.singletonList(standardDsType)); + + mockMvc.perform( + get(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + "/{targetTypeId}/compatibledistributionsettypes", + testType.getId()).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + + .andDo(this.document.document( + pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)), + responseFields(fieldWithPath("[]id").description(ApiModelPropertiesGeneric.ITEM_ID), + fieldWithPath("[]key").description(MgmtApiModelProperties.DS_TYPE_KEY), + fieldWithPath("[]name").description(ApiModelPropertiesGeneric.NAME), + fieldWithPath("[]description").description(ApiModelPropertiesGeneric.DESCRPTION), + fieldWithPath("[]createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), + fieldWithPath("[]createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), + fieldWithPath("[]lastModifiedAt") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"), + fieldWithPath("[]lastModifiedBy") + .description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"), + fieldWithPath("[]deleted").description(ApiModelPropertiesGeneric.DELETED), + fieldWithPath("[]_links.self").ignored()))); + + } + + @Test + @Description("Handles the POST request for adding a compatible distribution set type to a target type." + + " Required Permission: " + SpPermission.UPDATE_TARGET + " and " + SpPermission.READ_REPOSITORY) + public void postCompatibleDistributionSetTypes() throws Exception { + final DistributionSetType dsType1 = distributionSetTypeManagement.create( + entityFactory.distributionSetType().create().key("test1").name("TestName1").description("Desc1")); + final DistributionSetType dsType2 = distributionSetTypeManagement.create( + entityFactory.distributionSetType().create().key("test2").name("TestName2").description("Desc2")); + final TargetType targetType = testdataFactory.createTargetType("targetType", + Collections.singletonList(standardDsType)); + + mockMvc.perform( + post(MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + "/{targetTypeId}/compatibledistributionsettypes", + targetType.getId()) + .content("[{\"id\":" + dsType1.getId() + "},{\"id\":" + dsType2.getId() + "}]") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document( + pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)), + requestFields(requestFieldWithPath("[]id").description(ApiModelPropertiesGeneric.ITEM_ID)))); + } + + @Test + @Description("Handles the DELETE request to unassign the list of compatible distribution set types in that target type. " + + SpPermission.UPDATE_TARGET + " and " + SpPermission.READ_REPOSITORY) + public void deleteCompatibleDistributionSetType() throws Exception { + final DistributionSetType dsType = distributionSetTypeManagement.create( + entityFactory.distributionSetType().create().key("test1").name("TestName1").description("Desc1")); + final TargetType targetType = testdataFactory.createTargetType("targetType", Collections.singletonList(dsType)); + + mockMvc.perform(delete( + MgmtRestConstants.TARGETTYPE_V1_REQUEST_MAPPING + + "/{targetTypeId}/compatibledistributionsettypes/{distributionSetTypeId}", + targetType.getId(), dsType.getId()).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(this.document.document(pathParameters( + parameterWithName("targetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID), + parameterWithName("distributionSetTypeId").description(ApiModelPropertiesGeneric.ITEM_ID)))); + + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java index 420f1e828..222bbf36b 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java @@ -298,6 +298,11 @@ public class HawkbitSecurityProperties { */ private long maxArtifactStorage = 21_474_836_480L; + /** + * Maximum number of distribution set types per target types + */ + private int maxDistributionSetTypesPerTargetType = 50; + private final Filter filter = new Filter(); private final Filter uiFilter = new Filter(); @@ -439,6 +444,10 @@ public class HawkbitSecurityProperties { this.maxArtifactStorage = maxArtifactStorage; } + public int getMaxDistributionSetTypesPerTargetType() { + return maxDistributionSetTypesPerTargetType; + } + /** * Configuration for hawkBits DOS prevention filter. This is usually an * infrastructure topic (e.g. Web Application Firewall (WAF)) but might