diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index a7517d7d6..b0b446b5f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -12,6 +12,7 @@ public class RequestParamConstants { public static final String LANGUAGE = "language"; public static final String USER_ID = "userId"; public static final String AUTHORIZATION = "Authorization"; + public static final String X_AUTHORIZATION = "X-Authorization"; public static final String RESET_SYNC = "resetSync"; public static final String SHOW_DEV_VERSION = "isShowDevVersion"; public static final String DESIGNER_VERSION = "designerVersion"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java index 74ebf6d0a..3ad5c599d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -1,22 +1,31 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_BY_ID; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_RATING_BY_ID; -import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; -import static com.axonivy.market.constants.RequestParamConstants.ID; -import static com.axonivy.market.constants.RequestParamConstants.USER_ID; - -import java.net.URI; -import java.util.List; - +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.FeedbackModelRequest; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -28,20 +37,20 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.axonivy.market.assembler.FeedbackModelAssembler; -import com.axonivy.market.constants.CommonConstants; -import com.axonivy.market.entity.Feedback; -import com.axonivy.market.model.FeedbackModel; -import com.axonivy.market.model.ProductRating; -import com.axonivy.market.service.FeedbackService; -import com.axonivy.market.service.JwtService; +import java.net.URI; +import java.util.List; -import io.jsonwebtoken.Claims; -import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; +import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_RATING_BY_ID; +import static com.axonivy.market.constants.RequestParamConstants.X_AUTHORIZATION; +import static com.axonivy.market.constants.RequestParamConstants.ID; +import static com.axonivy.market.constants.RequestParamConstants.USER_ID; @RestController @RequestMapping(FEEDBACK) +@Tag(name = "User Feedback Controllers", description = "API collection to handle user's feedback.") public class FeedbackController { private final FeedbackService feedbackService; @@ -50,18 +59,21 @@ public class FeedbackController { private final PagedResourcesAssembler pagedResourcesAssembler; - public FeedbackController(FeedbackService feedbackService, JwtService jwtService, - FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { + public FeedbackController(FeedbackService feedbackService, JwtService jwtService, FeedbackModelAssembler feedbackModelAssembler, PagedResourcesAssembler pagedResourcesAssembler) { this.feedbackService = feedbackService; this.jwtService = jwtService; this.feedbackModelAssembler = feedbackModelAssembler; this.pagedResourcesAssembler = pagedResourcesAssembler; } - @Operation(summary = "Find all feedbacks by product id") @GetMapping(PRODUCT_BY_ID) - public ResponseEntity> findFeedbacks(@PathVariable(ID) String productId, - Pageable pageable) { + @Operation(summary = "Find feedbacks by product id with lazy loading", description = "Get all user feedback by product id (from meta.json) with lazy loading", parameters = { + @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", required = true), + @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", required = true), + @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true)}) + public ResponseEntity> findFeedbacks( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String productId, + @ParameterObject Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); @@ -72,25 +84,34 @@ public ResponseEntity> findFeedbacks(@PathVariable(ID) } @GetMapping(BY_ID) - public ResponseEntity findFeedback(@PathVariable(ID) String id) { + @Operation(summary = "Find all feedbacks by product id", description = "Get all feedbacks by product id(from meta.json) which is used in mobile viewport.") + public ResponseEntity findFeedback( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String id) { Feedback feedback = feedbackService.findFeedback(id); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } - @Operation(summary = "Find all feedbacks by user id and product id") @GetMapping() - public ResponseEntity findFeedbackByUserIdAndProductId(@RequestParam(USER_ID) String userId, - @RequestParam("productId") String productId) { + @Operation(summary = "Find all feedbacks by user id and product id", description = "Get current user feedback on target product.") + public ResponseEntity findFeedbackByUserIdAndProductId( + @RequestParam(USER_ID) @Parameter(description = "Id of current user from DB", example = "1234", in = ParameterIn.QUERY) String userId, + @RequestParam("productId") @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.QUERY) String productId) { Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); } @PostMapping - public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel feedback, - @RequestHeader(value = AUTHORIZATION) String authorizationHeader) { + @Operation(summary = "Create user feedback", description = "Save user feedback of product with their token from Github account.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Example request body for feedback", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = FeedbackModelRequest.class))) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created user feedback"), + @ApiResponse(responseCode = "401", description = "Unauthorized request")}) + public ResponseEntity createFeedback( + @RequestBody @Valid FeedbackModelRequest feedbackRequest, + @RequestHeader(value = X_AUTHORIZATION) @Parameter(description = "JWT Bearer token", example = "Bearer 123456", in = ParameterIn.HEADER) String bearerToken) { String token = null; - if (authorizationHeader != null && authorizationHeader.startsWith(CommonConstants.BEARER)) { - token = authorizationHeader.substring(CommonConstants.BEARER.length()).trim(); // Remove "Bearer " prefix + if (bearerToken != null && bearerToken.startsWith(CommonConstants.BEARER)) { + token = bearerToken.substring(CommonConstants.BEARER.length()).trim(); // Remove "Bearer " prefix } // Validate the token @@ -99,8 +120,7 @@ public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel fee } Claims claims = jwtService.getClaimsFromToken(token); - feedback.setUserId(claims.getSubject()); - Feedback newFeedback = feedbackService.upsertFeedback(feedback); + Feedback newFeedback = feedbackService.upsertFeedback(feedbackRequest, claims.getSubject()); URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(BY_ID).buildAndExpand(newFeedback.getId()) .toUri(); @@ -108,9 +128,10 @@ public ResponseEntity createFeedback(@RequestBody @Valid FeedbackModel fee return ResponseEntity.created(location).build(); } - @Operation(summary = "Find rating information of product by id") + @Operation(summary = "Find rating information of product by its id.", description = "Get overall rating of product by its id.") @GetMapping(PRODUCT_RATING_BY_ID) - public ResponseEntity> getProductRating(@PathVariable(ID) String productId) { + public ResponseEntity> getProductRating( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "portal", in = ParameterIn.PATH) String productId) { return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java index 968c81e2b..0547e5a74 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -7,7 +7,13 @@ import java.util.Collections; import java.util.Map; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -39,11 +45,15 @@ public OAuth2Controller(GitHubService gitHubService, JwtService jwtService, GitH } @PostMapping(GIT_HUB_LOGIN) + @Operation(description = "Get rating authentication token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully login to GitHub provider", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Bad Request")}) + @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Oauth2AuthorizationCode.class))) public ResponseEntity> gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { - String accessToken = EMPTY; + String accessToken; try { - GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), - gitHubProperty); + GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), gitHubProperty); accessToken = tokenResponse.getAccessToken(); } catch (Exception e) { return new ResponseEntity<>(Map.of(e.getClass().getName(), e.getMessage()), HttpStatus.BAD_REQUEST); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java index 6569985b5..f315ad6f4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductController.java @@ -1,5 +1,17 @@ package com.axonivy.market.controller; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; +import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; +import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; +import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; +import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; +import static com.axonivy.market.constants.RequestParamConstants.TYPE; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; import com.axonivy.market.assembler.ProductModelAssembler; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; @@ -13,6 +25,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import org.apache.commons.lang3.time.StopWatch; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -30,16 +43,10 @@ import org.springframework.web.bind.annotation.RestController; import static com.axonivy.market.constants.RequestMappingConstants.CUSTOM_SORT; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; -import static com.axonivy.market.constants.RequestParamConstants.AUTHORIZATION; -import static com.axonivy.market.constants.RequestParamConstants.KEYWORD; -import static com.axonivy.market.constants.RequestParamConstants.LANGUAGE; -import static com.axonivy.market.constants.RequestParamConstants.RESET_SYNC; -import static com.axonivy.market.constants.RequestParamConstants.TYPE; @RestController @RequestMapping(PRODUCT) +@Tag(name = "Product Controller", description = "API collection to get and search products") public class ProductController { private final ProductService productService; @@ -48,18 +55,24 @@ public class ProductController { private final PagedResourcesAssembler pagedResourcesAssembler; public ProductController(ProductService productService, GitHubService gitHubService, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { + PagedResourcesAssembler pagedResourcesAssembler) { this.productService = productService; this.gitHubService = gitHubService; this.assembler = assembler; this.pagedResourcesAssembler = pagedResourcesAssembler; } - @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() - public ResponseEntity> findProducts(@RequestParam(name = TYPE) String type, - @RequestParam(required = false, name = KEYWORD) String keyword, @RequestParam(name = LANGUAGE) String language, - Pageable pageable) { + @Operation(summary = "Retrieve a paginated list of all products, optionally filtered by type, keyword, and language", description = "By default, the system finds products with type 'all'", parameters = { + @Parameter(name = "page", description = "Page number to retrieve", in = ParameterIn.QUERY, example = "0", required = true), + @Parameter(name = "size", description = "Number of items per page", in = ParameterIn.QUERY, example = "20", required = true), + @Parameter(name = "sort", description = "Sorting criteria in the format: Sorting criteria(popularity|alphabetically|recent), Sorting order(asc|desc)", + in = ParameterIn.QUERY, example = "[\"popularity\",\"asc\"]", required = true)}) + public ResponseEntity> findProducts( + @RequestParam(name = TYPE) @Parameter(description = "Type of product.", in = ParameterIn.QUERY, schema = @Schema(type = "string", allowableValues = {"all", "connectors", "utilities", "solutions", "demos"})) String type, + @RequestParam(required = false, name = KEYWORD) @Parameter(description = "Keyword that exist in product's name or short description", example = "connector", in = ParameterIn.QUERY) String keyword, + @RequestParam(name = LANGUAGE) @Parameter(description = "Language of product short description", in = ParameterIn.QUERY, schema = @Schema(allowableValues = {"en", "de"})) String language, + @ParameterObject Pageable pageable) { Page results = productService.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); @@ -70,7 +83,9 @@ public ResponseEntity> findProducts(@RequestParam(name } @PutMapping(SYNC) - public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION) String authorizationHeader, + @Operation(hidden = true) + public ResponseEntity syncProducts( + @RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestParam(value = RESET_SYNC, required = false) Boolean resetSync) { String token = getBearerToken(authorizationHeader); gitHubService.validateUserOrganization(token, GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); @@ -94,6 +109,7 @@ public ResponseEntity syncProducts(@RequestHeader(value = AUTHORIZATION } @PostMapping(CUSTOM_SORT) + @Operation(hidden = true) public ResponseEntity createCustomSortProducts( @RequestHeader(value = AUTHORIZATION) String authorizationHeader, @RequestBody @Valid ProductCustomSortRequest productCustomSortRequest) { @@ -108,7 +124,7 @@ public ResponseEntity createCustomSortProducts( @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { var emptyPagedModel = (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), - ProductModel.class); + ProductModel.class); return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java index 58fb963f1..6242e5589 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDetailsController.java @@ -12,6 +12,9 @@ import java.util.List; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; @@ -32,43 +35,49 @@ @RestController @RequestMapping(PRODUCT_DETAILS) +@Tag(name = "Product Detail Controllers", description = "API collection to get product's detail.") public class ProductDetailsController { private final VersionService versionService; private final ProductService productService; private final ProductDetailModelAssembler detailModelAssembler; - public ProductDetailsController(VersionService versionService, ProductService productService, - ProductDetailModelAssembler detailModelAssembler) { + public ProductDetailsController(VersionService versionService, ProductService productService, ProductDetailModelAssembler detailModelAssembler) { this.versionService = versionService; this.productService = productService; this.detailModelAssembler = detailModelAssembler; } @GetMapping(BY_ID_AND_TAG) - public ResponseEntity findProductDetailsByVersion(@PathVariable(ID) String id, - @PathVariable(TAG) String tag) { + @Operation(summary = "Find product detail by product id and release tag.", description = "get product detail by it product id and release tag") + public ResponseEntity findProductDetailsByVersion( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id, + @PathVariable(TAG) @Parameter(description = "Release tag (from git hub repo tags)", example = "v10.0.20", in = ParameterIn.PATH) String tag) { var productDetail = productService.fetchProductDetail(id); return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); } - @Operation(summary = "increase installation count by 1", description = "update installation count when click download product files by users") @CrossOrigin(originPatterns = "*") @PutMapping(INSTALLATION_COUNT_BY_ID) - public ResponseEntity syncInstallationCount(@PathVariable(ID) String key) { - int result = productService.updateInstallationCountForProduct(key); + @Operation(summary = "Update installation count of product", description = "By default, increase installation count when click download product files by users") + public ResponseEntity syncInstallationCount( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String productId) { + int result = productService.updateInstallationCountForProduct(productId); return new ResponseEntity<>(result, HttpStatus.OK); } @GetMapping(BY_ID) - public ResponseEntity findProductDetails(@PathVariable(ID) String id) { + @Operation(summary = "increase installation count by 1", description = "update installation count when click download product files by users") + public ResponseEntity findProductDetails( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id) { var productDetail = productService.fetchProductDetail(id); return new ResponseEntity<>(detailModelAssembler.toModel(productDetail), HttpStatus.OK); } @GetMapping(VERSIONS_BY_ID) - public ResponseEntity> findProductVersionsById(@PathVariable(ID) String id, - @RequestParam(SHOW_DEV_VERSION) boolean isShowDevVersion, - @RequestParam(name = DESIGNER_VERSION, required = false) String designerVersion) { + public ResponseEntity> findProductVersionsById( + @PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String id, + @RequestParam(SHOW_DEV_VERSION) @Parameter(description = "Option to get Dev Version (Snapshot/ sprint release)", in = ParameterIn.QUERY) boolean isShowDevVersion, + @RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY, example = "v10.0.20") String designerVersion) { List models = versionService.getArtifactsAndVersionToDisplay(id, isShowDevVersion, designerVersion); return new ResponseEntity<>(models, HttpStatus.OK); diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java index 81a27e864..e1bb99383 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/MavenArtifactModel.java @@ -1,5 +1,6 @@ package com.axonivy.market.entity; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,7 +18,9 @@ public class MavenArtifactModel implements Serializable { @Serial private static final long serialVersionUID = 1L; + @Schema(description = "Display name and type of artifact", example = "Adobe Acrobat Sign Connector (.iar)") private String name; + @Schema(description = "Artifact download url", example = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.25/adobe-acrobat-sign-connector-10.0.25.iar") private String downloadUrl; @Transient private Boolean isProductArtifact; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java index bc48a39ad..43397d99a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -1,5 +1,6 @@ package com.axonivy.market.entity; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -16,13 +17,22 @@ public class ProductModuleContent implements Serializable { @Serial private static final long serialVersionUID = 1L; + @Schema(description = "Target release tag", example = "v10.0.25") private String tag; + @Schema(description = "Product detail description content ", example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }") private Map description; + @Schema(description = "Setup tab content", example = "Adobe Sign account creation: An Adobe Sign account needs to be created to setup and use the connector.") private String setup; + @Schema(description = "Demo tab content", example = "The demo project can be used to test the authentication and signing and the demo implementation can be used as inspiration for development") private String demo; + @Schema(description = "Is dependency artifact", example = "true") private Boolean isDependency; + @Schema(example = "Adobe Acrobat Sign Connector") private String name; + @Schema(description = "Product artifact's group id", example = "com.axonivy.connector.adobe.acrobat.sign") private String groupId; + @Schema(description = "Product artifact's artifact id", example = "adobe-acrobat-sign-connector-product") private String artifactId; + @Schema(description = "Artifact file type", example = "iar") private String type; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java index d687c1577..224de4e86 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java +++ b/marketplace-service/src/main/java/com/axonivy/market/factory/ProductFactory.java @@ -46,7 +46,7 @@ public static Product mappingByGHContent(Product product, GHContent content) { } public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { - Meta meta = null; + Meta meta; try { meta = jsonDecode(ghContent); } catch (Exception e) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java index 663fab1dc..eea330356 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java @@ -1,10 +1,7 @@ package com.axonivy.market.model; import com.fasterxml.jackson.annotation.JsonInclude; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -21,23 +18,34 @@ @Relation(collectionRelation = "feedbacks", itemRelation = "feedback") @JsonInclude(JsonInclude.Include.NON_NULL) public class FeedbackModel extends RepresentationModel { + @Schema(description = "Id of feedback", example = "667940ecc881b1d0db072f9e") private String id; + + @Schema(description = "User Id", example = "666ff14c847c664ac54d2643") private String userId; + + @Schema(description = "Github username", example = "ntqdinh-axonivy") private String username; + + @Schema(description = "Url of github avatar", example = "https://avatars.githubusercontent.com/u/1?v=4") private String userAvatarUrl; + + @Schema(description = "3rd party login provider", example = "GitHub") private String userProvider; - @NotBlank(message = "Product id cannot be blank") + @Schema(description = "Product id (from meta.json)", example = "portal") private String productId; - @NotBlank(message = "Content cannot be blank") - @Size(max = 5, message = "Content length must be up to 250 characters") + @Schema(description = "User's feedback content", example = "Pretty cool connector.") private String content; - @Min(value = 1, message = "Rating should not be less than 1") - @Max(value = 5, message = "Rating should not be greater than 5") + @Schema(description = "User's rating point of target product", example = "5", minimum = "1", maximum = "5") private Integer rating; + + @Schema(description = "Feedback/rating creating timestamp", example = "2024-06-24T00:00:00.000Z") private Date createdAt; + + @Schema(description = "Latest feedback/rating updating timestamp", example = "2024-06-24T00:00:00.000Z") private Date updatedAt; @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModelRequest.java b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModelRequest.java new file mode 100644 index 000000000..a6807dcd2 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModelRequest.java @@ -0,0 +1,27 @@ +package com.axonivy.market.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class FeedbackModelRequest { + @Schema(description = "Product id (from meta.json)", example = "portal") + @NotBlank(message = "Product id cannot be blank") + private String productId; + + @Schema(description = "User's feedback content", example = "Pretty cool connector.") + @NotBlank(message = "Content cannot be blank") + @Size(max = 250, message = "Content length must not exceed 250 characters") + private String content; + + @Schema(description = "User's rating point of target product", example = "5", minimum = "1", maximum = "5") + @Min(value = 1, message = "Rating should not be less than 1") + @Max(value = 5, message = "Rating should not be greater than 5") + private Integer rating; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java index 4acdc23ad..32ef52dcd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MavenArtifactVersionModel.java @@ -1,6 +1,7 @@ package com.axonivy.market.model; import com.axonivy.market.entity.MavenArtifactModel; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,6 +14,7 @@ @AllArgsConstructor @NoArgsConstructor public class MavenArtifactVersionModel { + @Schema(description = "Target version", example = "10.0.19") private String version; private List artifactsByVersion; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java index b73f8dc66..febebdb42 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java @@ -1,5 +1,6 @@ package com.axonivy.market.model; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -8,5 +9,6 @@ @Setter @NoArgsConstructor public class Oauth2AuthorizationCode { + @Schema(description = "Exchange code") private String code; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java index 99a13922f..bfe864dbd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -1,6 +1,7 @@ package com.axonivy.market.model; import com.axonivy.market.entity.ProductModuleContent; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,17 +12,28 @@ @Setter @NoArgsConstructor public class ProductDetailModel extends ProductModel { + @Schema(description = "Product vendor", example = "Axon Ivy AG") private String vendor; + @Schema(description = "Platform review", example = "4.5") private String platformReview; + @Schema(description = "Latest release version from maven", example = "v10.0.25") private String newestReleaseVersion; + @Schema(description = "Product cost", example = "Free") private String cost; + @Schema(description = "Source repository url", example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector") private String sourceUrl; + @Schema(description = "Status badge url", example = "https://github.com/axonivy-market/adobe-acrobat-sign-connector/actions/workflows/ci.yml/badge.svg") private String statusBadgeUrl; + @Schema(description = "Default language", example = "English") private String language; + @Schema(description = "Product industry", example = "Cross-Industry") private String industry; + @Schema(description = "Compatibility", example = "10.0+") private String compatibility; + @Schema(description = "Can contact us", example = "false") private Boolean contactUs; private ProductModuleContent productModuleContent; + @Schema(description = "Installation/download count", example = "0") private int installationCount; @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java index 1eeb4f3bc..74fbe9471 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductModel.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -19,11 +20,17 @@ @Relation(collectionRelation = "products", itemRelation = "product") @JsonInclude(Include.NON_NULL) public class ProductModel extends RepresentationModel { + @Schema(description = "Product id", example = "jira-connector") private String id; + @Schema(description = "Product name by locale", example = "{ \"de\": \"Atlassian Jira\", \"en\": \"Atlassian Jira\" }") private Map names; + @Schema(description = "Product's short descriptions by locale", example = "{ \"de\": \"Nutze den Jira Connector von Atlassian, um Jira-Tickets direkt von der Axon Ivy Plattform aus zu verfolgen.\", \"en\": \"Atlassian's Jira connector lets you track issues directly from the Axon Ivy platform\" }") private Map shortDescriptions; + @Schema(description = "Product's logo url", example = "https://raw.githubusercontent.com/axonivy-market/market/feature/MARP-463-Multilingualism-for-Website/market/connector/jira/logo.png") private String logoUrl; + @Schema(description = "Type of product", example = "connector") private String type; + @Schema(description = "Tags of product", example = "[\"helper\"]") private List tags; @Override diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java index b151e05f4..e53967946 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java @@ -1,5 +1,6 @@ package com.axonivy.market.model; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,7 +11,10 @@ @AllArgsConstructor @NoArgsConstructor public class ProductRating { + @Schema(description = "Specific rating point of product", example = "3") private Integer starRating; + @Schema(description = "Count of rating on this specific point", example = "20") private Integer commentNumber; + @Schema(description = "Weight ration of this point/ total point", example = "20") private Integer percent; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java index b7d1785b5..435ada5a0 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java @@ -2,7 +2,7 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.FeedbackModelRequest; import com.axonivy.market.model.ProductRating; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -16,7 +16,7 @@ public interface FeedbackService { Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException; - Feedback upsertFeedback(FeedbackModel feedback) throws NotFoundException; + Feedback upsertFeedback(FeedbackModelRequest feedback, String userId) throws NotFoundException; List getProductRatingById(String productId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java index 77e007e1b..b4148918c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -3,7 +3,7 @@ import com.axonivy.market.entity.Feedback; import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.FeedbackModelRequest; import com.axonivy.market.model.ProductRating; import com.axonivy.market.repository.FeedbackRepository; import com.axonivy.market.repository.ProductRepository; @@ -58,14 +58,14 @@ public Feedback findFeedbackByUserIdAndProductId(String userId, String productId } @Override - public Feedback upsertFeedback(FeedbackModel feedback) throws NotFoundException { - validateUserExists(feedback.getUserId()); + public Feedback upsertFeedback(FeedbackModelRequest feedback, String userId) throws NotFoundException { + validateUserExists(userId); - Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, feedback.getProductId()); if (existingUserFeedback == null) { Feedback newFeedback = new Feedback(); - newFeedback.setUserId(feedback.getUserId()); + newFeedback.setUserId(userId); newFeedback.setProductId(feedback.getProductId()); newFeedback.setRating(feedback.getRating()); newFeedback.setContent(feedback.getContent()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java index c58fd8b8b..d001550b9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ProductServiceImpl.java @@ -214,7 +214,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { } private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { - Product result = null; + Product result; switch (file.getStatus()) { case MODIFIED, ADDED: var searchCriteria = new ProductSearchCriteria(); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java index 1af06b839..78819a592 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java @@ -3,7 +3,7 @@ import com.axonivy.market.assembler.FeedbackModelAssembler; import com.axonivy.market.entity.Feedback; import com.axonivy.market.entity.User; -import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.FeedbackModelRequest; import com.axonivy.market.service.FeedbackService; import com.axonivy.market.service.JwtService; import com.axonivy.market.service.UserService; @@ -122,14 +122,14 @@ void testFindFeedbackByUserIdAndProductId() { @Test void testCreateFeedback() { - FeedbackModel mockFeedbackModel = createFeedbackModelMock(); + FeedbackModelRequest mockFeedbackModel = createFeedbackModelRequestMock(); Feedback mockFeedback = createFeedbackMock(); Claims mockClaims = createMockClaims(); MockHttpServletRequest request = new MockHttpServletRequest(); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); when(jwtService.validateToken(TOKEN_SAMPLE)).thenReturn(true); when(jwtService.getClaimsFromToken(TOKEN_SAMPLE)).thenReturn(mockClaims); - when(service.upsertFeedback(any())).thenReturn(mockFeedback); + when(service.upsertFeedback(any(), any())).thenReturn(mockFeedback); var result = feedbackController.createFeedback(mockFeedbackModel, "Bearer " + TOKEN_SAMPLE); assertEquals(HttpStatus.CREATED, result.getStatusCode()); @@ -146,10 +146,8 @@ private Feedback createFeedbackMock() { return mockFeedback; } - private FeedbackModel createFeedbackModelMock() { - FeedbackModel mockFeedback = new FeedbackModel(); - mockFeedback.setId(FEEDBACK_ID_SAMPLE); - mockFeedback.setUserId(USER_ID_SAMPLE); + private FeedbackModelRequest createFeedbackModelRequestMock() { + FeedbackModelRequest mockFeedback = new FeedbackModelRequest(); mockFeedback.setProductId(PRODUCT_ID_SAMPLE); mockFeedback.setContent("Great product!"); mockFeedback.setRating(5); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java index 75f97b562..9bf3084d7 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -6,6 +6,7 @@ import com.axonivy.market.enums.ErrorCode; import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.model.FeedbackModelRequest; import com.axonivy.market.model.ProductRating; import com.axonivy.market.repository.FeedbackRepository; import com.axonivy.market.repository.ProductRepository; @@ -52,21 +53,31 @@ class FeedbackServiceImplTest { private Feedback feedback; private FeedbackModel feedbackModel; + private FeedbackModelRequest feedbackModelRequest; + private String userId; + @BeforeEach void setUp() { + userId = "user1"; + feedback = new Feedback(); feedback.setId("1"); - feedback.setUserId("user1"); + feedback.setUserId(userId); feedback.setProductId("product1"); feedback.setRating(5); feedback.setContent("Great product!"); feedbackModel = new FeedbackModel(); - feedbackModel.setUserId("user1"); + feedbackModel.setUserId(userId); feedbackModel.setProductId("product1"); feedbackModel.setRating(5); feedbackModel.setContent("Great product!"); + + feedbackModelRequest = new FeedbackModelRequest(); + feedbackModelRequest.setProductId("product1"); + feedbackModelRequest.setRating(5); + feedbackModelRequest.setContent("Great product!"); } @Test @@ -159,14 +170,13 @@ void testFindFeedbackByUserIdAndProductId_NotFound() { @Test void testUpsertFeedback_Insert() throws NotFoundException { - String userId = "user1"; String productId = "product1"; when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); when(feedbackRepository.findByUserIdAndProductId(userId, productId)).thenReturn(null); when(feedbackRepository.save(any(Feedback.class))).thenReturn(feedback); - Feedback result = feedbackService.upsertFeedback(feedbackModel); + Feedback result = feedbackService.upsertFeedback(feedbackModelRequest, userId); assertNotNull(result); assertEquals(feedbackModel.getUserId(), result.getUserId()); assertEquals(feedbackModel.getProductId(), result.getProductId()); @@ -179,14 +189,13 @@ void testUpsertFeedback_Insert() throws NotFoundException { @Test void testUpsertFeedback_Update() throws NotFoundException { - String userId = "user1"; String productId = "product1"; when(userRepository.findById(userId)).thenReturn(Optional.of(new User())); when(feedbackRepository.findByUserIdAndProductId(userId, productId)).thenReturn(feedback); when(feedbackRepository.save(any(Feedback.class))).thenReturn(feedback); - Feedback result = feedbackService.upsertFeedback(feedbackModel); + Feedback result = feedbackService.upsertFeedback(feedbackModelRequest, userId); assertNotNull(result); assertEquals(feedbackModel.getUserId(), result.getUserId()); assertEquals(feedbackModel.getProductId(), result.getProductId()); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts index 45ac6ef43..258f434cf 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.spec.ts @@ -55,7 +55,7 @@ describe('ProductFeedbackService', () => { const req = httpMock.expectOne('api/feedback'); expect(req.request.method).toBe('POST'); - expect(req.request.headers.get('Authorization')).toBe('Bearer mockToken'); + expect(req.request.headers.get('X-Authorization')).toBe('Bearer mockToken'); req.flush(feedback); expect(productStarRatingService.fetchData).toHaveBeenCalled(); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts index f21ea75c3..3236febe2 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-feedback/product-feedbacks-panel/product-feedback.service.ts @@ -47,7 +47,7 @@ export class ProductFeedbackService { submitFeedback(feedback: Feedback): Observable { const headers = new HttpHeaders().set( - 'Authorization', + 'X-Authorization', `Bearer ${this.authService.getToken()}` ); return this.http