From 8dfcb3b5d7934adab85f599adfc0ac48bf81d053 Mon Sep 17 00:00:00 2001 From: Thuy Nguyen <145430420+nntthuy-axonivy@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:45:23 +0700 Subject: [PATCH 1/5] MARP-572 Detail pages for new market main content (#24) Introduce get contents from folder product of release tag --- .../ProductDetailModelAssembler.java | 72 ++ .../assembler/ProductModelAssembler.java | 5 +- .../market/constants/CommonConstants.java | 7 +- .../market/constants/MavenConstants.java | 23 +- .../market/constants/MetaConstants.java | 11 + .../NonStandardProductPackageConstants.java | 39 +- .../constants/ProductJsonConstants.java | 32 +- .../market/constants/ReadmeConstants.java | 12 + .../market/controller/ProductController.java | 4 +- .../controller/ProductDetailsController.java | 58 +- .../com/axonivy/market/entity/Product.java | 95 +- .../market/entity/ProductModuleContent.java | 25 + .../market/factory/ProductFactory.java | 152 +-- .../com/axonivy/market/github/model/Meta.java | 4 +- .../service/GHAxonIvyProductRepoService.java | 14 +- .../impl/GHAxonIvyProductRepoServiceImpl.java | 370 ++++-- .../market/github/util/GitHubUtils.java | 85 ++ .../market/model/ProductDetailModel.java | 38 + .../market/repository/ProductRepository.java | 2 + .../market/service/ProductService.java | 4 + .../service/impl/ProductServiceImpl.java | 168 ++- .../service/impl/VersionServiceImpl.java | 601 +++++----- .../axonivy/market/util/XmlReaderUtils.java | 58 + .../axonivy/market/utils/XmlReaderUtils.java | 59 - .../ProductDetailsControllerTest.java | 77 +- .../market/factory/ProductFactoryTest.java | 4 +- .../GHAxonIvyProductRepoServiceImplTest.java | 242 ---- .../GHAxonIvyProductRepoServiceImplTest.java | 375 ++++++ .../service/ProductServiceImplTest.java | 104 +- .../service/VersionServiceImplTest.java | 1036 ++++++++--------- .../axonivy/market/util/GitHubUtilsTest.java | 93 ++ .../{utils => util}/XmlReaderUtilsTest.java | 16 +- 32 files changed, 2308 insertions(+), 1577 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java rename marketplace-service/src/test/java/com/axonivy/market/{utils => util}/XmlReaderUtilsTest.java (58%) diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java new file mode 100644 index 000000000..dbfa0be0b --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -0,0 +1,72 @@ +package com.axonivy.market.assembler; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import com.axonivy.market.controller.ProductDetailsController; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.model.ProductDetailModel; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ProductDetailModelAssembler extends RepresentationModelAssemblerSupport { + + private final ProductModelAssembler productModelAssembler; + + public ProductDetailModelAssembler(ProductModelAssembler productModelAssembler) { + super(ProductDetailsController.class, ProductDetailModel.class); + this.productModelAssembler = productModelAssembler; + } + + @Override + public ProductDetailModel toModel(Product product) { + return createModel(product, null); + } + + public ProductDetailModel toModel(Product product, String tag) { + return createModel(product, tag); + } + + private ProductDetailModel createModel(Product product, String tag) { + ResponseEntity selfLinkWithTag; + ProductDetailModel model = instantiateModel(product); + productModelAssembler.createResource(model, product); + if (StringUtils.isBlank(tag)) { + selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetails(product.getId()); + } else { + selfLinkWithTag = methodOn(ProductDetailsController.class).findProductDetailsByVersion(product.getId(), tag); + } + model.add(linkTo(selfLinkWithTag).withSelfRel()); + createDetailResource(model, product, tag); + return model; + } + + private void createDetailResource(ProductDetailModel model, Product product, String tag) { + model.setVendor(product.getVendor()); + model.setNewestReleaseVersion(product.getNewestReleaseVersion()); + model.setPlatformReview(product.getPlatformReview()); + model.setSourceUrl(product.getSourceUrl()); + model.setStatusBadgeUrl(product.getStatusBadgeUrl()); + model.setLanguage(product.getLanguage()); + model.setIndustry(product.getIndustry()); + model.setCompatibility(product.getCompatibility()); + model.setContactUs(product.getContactUs()); + model.setCost(product.getCost()); + + if (StringUtils.isBlank(tag) && StringUtils.isNotBlank(product.getNewestReleaseVersion())) { + tag = product.getNewestReleaseVersion(); + } + ProductModuleContent content = getProductModuleContentByTag(product.getProductModuleContents(), tag); + model.setProductModuleContent(content); + } + + private ProductModuleContent getProductModuleContentByTag(List contents, String tag) { + return contents.stream().filter(content -> StringUtils.equals(content.getTag(), tag)).findAny().orElse(null); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index bd9c948cd..00b94a52f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -20,12 +20,11 @@ public ProductModelAssembler() { @Override public ProductModel toModel(Product product) { ProductModel resource = new ProductModel(); - resource.add(linkTo(methodOn(ProductDetailsController.class).findProduct(product.getId(), product.getType())) - .withSelfRel()); + resource.add(linkTo(methodOn(ProductDetailsController.class).findProductDetails(product.getId())).withSelfRel()); return createResource(resource, product); } - private ProductModel createResource(ProductModel model, Product product) { + public ProductModel createResource(ProductModel model, Product product) { model.setId(product.getId()); model.setNames(product.getNames()); model.setShortDescriptions(product.getShortDescriptions()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java index d0e28028e..5f6eea10d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/CommonConstants.java @@ -7,8 +7,11 @@ public class CommonConstants { public static final int INITIAL_PAGE = 1; public static final int INITIAL_PAGE_SIZE = 10; - public static final String SLASH = "/"; public static final String REQUESTED_BY = "X-Requested-By"; - public static final String META_FILE = "meta.json"; public static final String LOGO_FILE = "logo.png"; + public static final String SLASH = "/"; + public static final String DOT_SEPARATOR = "."; + public static final String PLUS = "+"; + public static final String DASH_SEPARATOR = "-"; + public static final String SPACE_SEPARATOR = " "; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java index 992a4289b..4ba05471d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MavenConstants.java @@ -1,19 +1,14 @@ package com.axonivy.market.constants; public class MavenConstants { - private MavenConstants() { - } + private MavenConstants() {} - public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; - public static final String SPRINT_RELEASE_POSTFIX = "-m"; - public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; - public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; - public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; - public static final String DOT_SEPARATOR = "."; - public static final String GROUP_ID_URL_SEPARATOR = "/"; - public static final String ARTIFACT_ID_SEPARATOR = "-"; - public static final String ARTIFACT_NAME_SEPARATOR = " "; - public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; - public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; - public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; + public static final String SNAPSHOT_RELEASE_POSTFIX = "-SNAPSHOT"; + public static final String SPRINT_RELEASE_POSTFIX = "-m"; + public static final String PRODUCT_ARTIFACT_POSTFIX = "-product"; + public static final String METADATA_URL_FORMAT = "%s/%s/%s/maven-metadata.xml"; + public static final String DEFAULT_IVY_MAVEN_BASE_URL = "https://maven.axonivy.com"; + public static final String ARTIFACT_DOWNLOAD_URL_FORMAT = "%s/%s/%s/%s/%s-%s.%s"; + public static final String ARTIFACT_NAME_FORMAT = "%s (%s)"; + public static final String VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE = "//versions/version/text()"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java new file mode 100644 index 000000000..3b088f957 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MetaConstants.java @@ -0,0 +1,11 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MetaConstants { + public static final String META_FILE = "meta.json"; + public static final String DEFAULT_VENDOR_NAME = "Axon Ivy AG"; + public static final String DEFAULT_VENDOR_URL = "https://www.axonivy.com"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java index c2e14744e..133ff55ff 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/NonStandardProductPackageConstants.java @@ -1,20 +1,27 @@ package com.axonivy.market.constants; public class NonStandardProductPackageConstants { - private NonStandardProductPackageConstants() { - } + private NonStandardProductPackageConstants() {} - public static final String PORTAL = "portal"; - public static final String MICROSOFT_365 = ""; // No meta.json - public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json - public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json - public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json - public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json - public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; - public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json - public static final String ERROR_HANDLING = "error-handling-demo"; - public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; - public static final String WORKFLOW_DEMO = "workflow-demo"; - public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; - public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json -} \ No newline at end of file + public static final String PORTAL = "portal"; + public static final String MICROSOFT_REPO_NAME = "msgraph-connector"; + public static final String MICROSOFT_365 = "msgraph"; // No meta.json + public static final String MICROSOFT_CALENDAR = "msgraph-calendar"; // no fix product json + public static final String MICROSOFT_MAIL = "msgraph-mail";// no fix product json + public static final String MICROSOFT_TEAMS = "msgraph-chat";// no fix product json + public static final String MICROSOFT_TODO = "msgraph-todo";// no fix product json + public static final String CONNECTIVITY_FEATURE = "connectivity-demo"; + public static final String EMPLOYEE_ONBOARDING = "employee-onboarding"; // Invalid meta.json + public static final String ERROR_HANDLING = "error-handling-demo"; + public static final String RULE_ENGINE_DEMOS = "rule-engine-demo"; + public static final String WORKFLOW_DEMO = "workflow-demo"; + public static final String HTML_DIALOG_DEMO = "html-dialog-demo"; + public static final String PROCESSING_VALVE_DEMO = "processing-valve-demo";// no product json + public static final String OPENAI_CONNECTOR = "openai-connector"; + public static final String OPENAI_ASSISTANT = "openai-assistant"; + // Non standard image folder name + public static final String EXCEL_IMPORTER = "excel-importer"; + public static final String EXPRESS_IMPORTER = "express-importer"; + public static final String GRAPHQL_DEMO = "graphql-demo"; + public static final String DEEPL_CONNECTOR = "deepl-connector"; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java index 9ce62f956..96c6eb5e1 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ProductJsonConstants.java @@ -1,21 +1,21 @@ package com.axonivy.market.constants; public class ProductJsonConstants { + public static final String PRODUCT_JSON_FILE = "product.json"; + public static final String DATA = "data"; + public static final String REPOSITORIES = "repositories"; + public static final String URL = "url"; + public static final String ID = "id"; + public static final String PROJECTS = "projects"; + public static final String ARTIFACT_ID = "artifactId"; + public static final String GROUP_ID = "groupId"; + public static final String TYPE = "type"; + public static final String DEPENDENCIES = "dependencies"; + public static final String INSTALLERS = "installers"; + public static final String DEPENDENCY_SUFFIX = "-dependency"; + public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; + public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; + public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; - public static final String DATA = "data"; - public static final String REPOSITORIES = "repositories"; - public static final String URL = "url"; - public static final String ID = "id"; - public static final String PROJECTS = "projects"; - public static final String ARTIFACT_ID = "artifactId"; - public static final String GROUP_ID = "groupId"; - public static final String TYPE = "type"; - public static final String DEPENDENCIES = "dependencies"; - public static final String INSTALLERS = "installers"; - public static final String MAVEN_IMPORT_INSTALLER_ID = "maven-import"; - public static final String MAVEN_DROPIN_INSTALLER_ID = "maven-dropins"; - public static final String MAVEN_DEPENDENCY_INSTALLER_ID = "maven-dependency"; - - private ProductJsonConstants() { - } + private ProductJsonConstants() {} } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java new file mode 100644 index 000000000..6d3024e9f --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ReadmeConstants.java @@ -0,0 +1,12 @@ +package com.axonivy.market.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReadmeConstants { + public static final String IMAGES = "images"; + public static final String README_FILE = "README.md"; + public static final String DEMO_PART = "## Demo"; + public static final String SETUP_PART = "## Setup"; +} 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 55d0444ff..ce273cd33 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 @@ -75,8 +75,8 @@ public ResponseEntity syncProducts() { @SuppressWarnings("unchecked") private ResponseEntity> generateEmptyPagedModel() { - var emptyPagedModel = (PagedModel) pagedResourcesAssembler - .toEmptyModel(Page.empty(), ProductModel.class); + var emptyPagedModel = + (PagedModel) pagedResourcesAssembler.toEmptyModel(Page.empty(), 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 c5536cd48..e83712718 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 @@ -8,6 +8,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; + import org.springframework.web.bind.annotation.PathVariable; import java.util.List; @@ -17,24 +21,36 @@ @RestController @RequestMapping(PRODUCT_DETAILS) public class ProductDetailsController { - private final VersionService service; - - public ProductDetailsController(VersionService service) { - this.service = service; - } - - @GetMapping("/{id}") - public ResponseEntity findProduct(@PathVariable("id") String key, - @RequestParam(name = "type", required = false) String type) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - @GetMapping("/{id}/versions") - public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, - @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, - @RequestParam(name = "designerVersion", required = false) String designerVersion) { - List models = service.getArtifactsAndVersionToDisplay(id, isShowDevVersion, - designerVersion); - return new ResponseEntity<>(models, HttpStatus.OK); - } -} \ No newline at end of file + private final VersionService versionService; + private final ProductService productService; + private final ProductDetailModelAssembler detailModelAssembler; + + public ProductDetailsController(VersionService versionService, ProductService productService, + ProductDetailModelAssembler detailModelAssembler) { + this.versionService = versionService; + this.productService = productService; + this.detailModelAssembler = detailModelAssembler; + } + + @GetMapping("/{id}/{tag}") + public ResponseEntity findProductDetailsByVersion(@PathVariable("id") String id, + @PathVariable("tag") String tag) { + var productDetail = productService.fetchProductDetail(id); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity findProductDetails(@PathVariable("id") String id) { + var productDetail = productService.fetchProductDetail(id); + return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, null), HttpStatus.OK); + } + + @GetMapping("/{id}/versions") + public ResponseEntity> findProductVersionsById(@PathVariable("id") String id, + @RequestParam(name = "isShowDevVersion") boolean isShowDevVersion, + @RequestParam(name = "designerVersion", required = false) 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/Product.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java index 9d89381f7..deb469a61 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Product.java @@ -6,69 +6,68 @@ import java.util.Date; import java.util.List; -import com.axonivy.market.github.model.MavenArtifact; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import com.axonivy.market.github.model.MavenArtifact; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Document(PRODUCT) public class Product implements Serializable { - - private static final long serialVersionUID = -8770801877877277258L; - @Id - private String id; - private String marketDirectory; + private static final long serialVersionUID = -8770801877877277258L; + @Id + private String id; + private String marketDirectory; @JsonProperty - private MultilingualismValue names; - private String version; + private MultilingualismValue names; + private String version; @JsonProperty - private MultilingualismValue shortDescriptions; - private String logoUrl; - private Boolean listed; - private String type; - private List tags; - private String vendor; - private String vendorImage; - private String vendorUrl; - private String platformReview; - private String cost; - private String repositoryName; - private String sourceUrl; - private String statusBadgeUrl; - private String language; - private String industry; - private String compatibility; - private Boolean validate; - private Boolean contactUs; - private Integer installationCount; - private Date newestPublishedDate; - private String newestReleaseVersion; - private List artifacts; + private MultilingualismValue shortDescriptions; + private String logoUrl; + private Boolean listed; + private String type; + private List tags; + private String vendor; + private String vendorUrl; + private String platformReview; + private String cost; + private String repositoryName; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean validate; + private Boolean contactUs; + private Integer installationCount; + private Date newestPublishedDate; + private String newestReleaseVersion; + private List productModuleContents; + private List artifacts; - @Override - public int hashCode() { - return new HashCodeBuilder().append(id).hashCode(); - } + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } - @Override - public boolean equals(Object obj) { - if (obj == null || this.getClass() != obj.getClass()) { - return false; - } - return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); - } + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((Product) obj).getId()).isEquals(); + } -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..d2b6d1145 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -0,0 +1,25 @@ +package com.axonivy.market.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProductModuleContent implements Serializable { + private static final long serialVersionUID = 1L; + private String tag; + private String description; + private String setup; + private String demo; + private Boolean isDependency; + private String name; + private String groupId; + private String artifactId; + 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 4cd0f6971..38e16a438 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 @@ -1,77 +1,81 @@ package com.axonivy.market.factory; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.*; import static org.apache.commons.lang3.StringUtils.EMPTY; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.model.DisplayValue; +import com.axonivy.market.model.MultilingualismValue; +import org.apache.commons.lang3.BooleanUtils; + import java.io.IOException; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; -import org.springframework.util.CollectionUtils; import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.Language; import com.axonivy.market.github.model.Meta; -import com.axonivy.market.github.util.GitHubUtils; -import com.axonivy.market.model.DisplayValue; -import com.axonivy.market.model.MultilingualismValue; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.util.CollectionUtils; @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - public static Product mappingByGHContent(Product product, GHContent content) { - if (content == null) { - return product; - } - - var contentName = content.getName(); - if (StringUtils.endsWith(contentName, META_FILE)) { - mappingByMetaJSONFile(product, content); - } - if (StringUtils.endsWith(contentName, LOGO_FILE)) { - product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); - } - return product; - } - - public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { - Meta meta = null; - try { - meta = jsonDecode(ghContent); - } catch (Exception e) { - log.error("Mapping from Meta file by GHContent failed", e); - return product; - } - - product.setId(meta.getId()); - product.setNames(mappingMultilingualismValueByMetaJSONFile(meta.getNames())); - product.setMarketDirectory(extractParentDirectory(ghContent)); - product.setListed(meta.getListed()); - product.setType(meta.getType()); - product.setTags(meta.getTags()); - product.setVersion(meta.getVersion()); - product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); - product.setVendor(meta.getVendor()); - product.setVendorImage(meta.getVendorImage()); - product.setVendorUrl(meta.getVendorUrl()); - product.setPlatformReview(meta.getPlatformReview()); - product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); - product.setLanguage(meta.getLanguage()); - product.setIndustry(meta.getIndustry()); - extractSourceUrl(product, meta); - product.setArtifacts(meta.getMavenArtifacts()); - return product; - } + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static Product mappingByGHContent(Product product, GHContent content) { + if (content == null) { + return product; + } + + var contentName = content.getName(); + if (StringUtils.endsWith(contentName, META_FILE)) { + mappingByMetaJSONFile(product, content); + } + if (StringUtils.endsWith(contentName, LOGO_FILE)) { + product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); + } + return product; + } + + public static Product mappingByMetaJSONFile(Product product, GHContent ghContent) { + Meta meta = null; + try { + meta = jsonDecode(ghContent); + } catch (Exception e) { + log.error("Mapping from Meta file by GHContent failed", e); + return product; + } + + product.setId(meta.getId()); + product.setNames(mappingMultilingualismValueByMetaJSONFile(meta.getNames())); + product.setMarketDirectory(extractParentDirectory(ghContent)); + product.setListed(meta.getListed()); + product.setType(meta.getType()); + product.setTags(meta.getTags()); + product.setVersion(meta.getVersion()); + product.setShortDescriptions(mappingMultilingualismValueByMetaJSONFile(meta.getDescriptions())); + product.setVendor(StringUtils.isBlank(meta.getVendor()) ? DEFAULT_VENDOR_NAME : meta.getVendor()); + product.setVendorUrl(StringUtils.isBlank(meta.getVendorUrl()) ? DEFAULT_VENDOR_URL : meta.getVendorUrl()); + product.setPlatformReview(meta.getPlatformReview()); + product.setStatusBadgeUrl(meta.getStatusBadgeUrl()); + product.setLanguage(meta.getLanguage()); + product.setIndustry(meta.getIndustry()); + product.setContactUs(BooleanUtils.isTrue(meta.getContactUs())); + product.setCost(StringUtils.isBlank(meta.getCost()) ? "Free" : StringUtils.capitalize(meta.getCost())); + product.setCompatibility(meta.getCompatibility()); + extractSourceUrl(product, meta); + product.setArtifacts(meta.getMavenArtifacts()); + return product; + } private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(List list) { MultilingualismValue value = new MultilingualismValue(); @@ -88,27 +92,27 @@ private static MultilingualismValue mappingMultilingualismValueByMetaJSONFile(Li return value; } - private static String extractParentDirectory(GHContent ghContent) { - var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); - return path.replace(ghContent.getName(), EMPTY); - } - - public static void extractSourceUrl(Product product, Meta meta) { - var sourceUrl = meta.getSourceUrl(); - if (StringUtils.isBlank(sourceUrl)) { - return; - } - String[] tokens = sourceUrl.split(SLASH); - var tokensLength = tokens.length; - var repositoryPath = sourceUrl; - if (tokensLength > 1) { - repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); - } - product.setRepositoryName(repositoryPath); - product.setSourceUrl(sourceUrl); - } - - private static Meta jsonDecode(GHContent ghContent) throws IOException { - return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); - } + private static String extractParentDirectory(GHContent ghContent) { + var path = StringUtils.defaultIfEmpty(ghContent.getPath(), EMPTY); + return path.replace(ghContent.getName(), EMPTY); + } + + public static void extractSourceUrl(Product product, Meta meta) { + var sourceUrl = meta.getSourceUrl(); + if (StringUtils.isBlank(sourceUrl)) { + return; + } + String[] tokens = sourceUrl.split(SLASH); + var tokensLength = tokens.length; + var repositoryPath = sourceUrl; + if (tokensLength > 1) { + repositoryPath = String.join(SLASH, tokens[tokensLength - 2], tokens[tokensLength - 1]); + } + product.setRepositoryName(repositoryPath); + product.setSourceUrl(sourceUrl); + } + + private static Meta jsonDecode(GHContent ghContent) throws IOException { + return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index ee80857de..92e940487 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -31,8 +31,10 @@ public class Meta { private Boolean listed; private String version; private String vendor; - private String vendorImage; private String vendorUrl; private List tags; private List mavenArtifacts; + private String compatibility; + private Boolean contactUs; + private String cost; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java index 3a9e85180..46afa0f0c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyProductRepoService.java @@ -1,7 +1,11 @@ package com.axonivy.market.github.service; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.github.model.MavenArtifact; + import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; import java.io.IOException; @@ -9,9 +13,11 @@ public interface GHAxonIvyProductRepoService { - GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); + GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion); + + List getAllTagsFromRepoName(String repoName) throws IOException; - List getAllTagsFromRepoName(String repoName) throws IOException; + ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, String tag); - List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; -} \ No newline at end of file + List convertProductJsonToMavenProductInfo(GHContent content) throws IOException; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java index 6b6b8c38e..ba3d79740 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyProductRepoServiceImpl.java @@ -1,127 +1,285 @@ package com.axonivy.market.github.service.impl; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import java.io.IOException; +import java.util.*; + +import com.axonivy.market.constants.*; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.github.util.GitHubUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; import lombok.extern.log4j.Log4j2; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import com.axonivy.market.github.model.MavenArtifact; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTag; import org.springframework.stereotype.Service; import com.axonivy.market.github.service.GitHubService; +import org.springframework.util.CollectionUtils; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Log4j2 @Service public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { - private GHOrganization organization; - private final GitHubService gitHubService; - private String repoUrl; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { - this.gitHubService = gitHubService; - } - - @Override - public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { - List artifacts = new ArrayList<>(); - InputStream contentStream = extractedContentStream(content); - if (Objects.isNull(contentStream)) { - return artifacts; - } - - JsonNode rootNode = objectMapper.readTree(contentStream); - JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); - - for (JsonNode mavenNode : installersNode) { - JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); - - // Not convert to artifact if id of node is not maven-import or maven-dependency - List installerIdsToDisplay = List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, - ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); - if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { - continue; - } - - // Extract repository URL - JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); - repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); - - // Process projects - if (dataNode.has(ProductJsonConstants.PROJECTS)) { - extractMavenArtifactFromJsonNode(dataNode, false, artifacts); - } - - // Process dependencies - if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { - extractMavenArtifactFromJsonNode(dataNode, true, artifacts); - } - } - return artifacts; - } - - public InputStream extractedContentStream(GHContent content) { - try { - return content.read(); - } catch (IOException | NullPointerException e) { - log.warn("Can not read the current content: {}", e.getMessage()); - return null; - } - } - - public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, - List artifacts) { - String nodeName = ProductJsonConstants.PROJECTS; - if (isDependency) { - nodeName = ProductJsonConstants.DEPENDENCIES; - } - JsonNode dependenciesNode = dataNode.path(nodeName); - for (JsonNode dependencyNode : dependenciesNode) { - MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); - artifacts.add(artifact); - } - } - - public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { - MavenArtifact artifact = new MavenArtifact(); - artifact.setRepoUrl(repoUrl); - artifact.setIsDependency(isDependency); - artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); - artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); - artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); - artifact.setIsProductArtifact(true); - return artifact; - } - - @Override - public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { - try { - return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); - } catch (IOException e) { - log.error("Cannot Get Content From File Directory", e); - return null; - } - } - - public GHOrganization getOrganization() throws IOException { - if (organization == null) { - organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); - } - return organization; - } - - @Override - public List getAllTagsFromRepoName(String repoName) throws IOException { - return getOrganization().getRepository(repoName).listTags().toList(); - } + private GHOrganization organization; + private final GitHubService gitHubService; + private String repoUrl; + private static final ObjectMapper objectMapper = new ObjectMapper(); + public static final String DEMO_SETUP_TITLE = "(?i)## Demo|## Setup"; + public static final String IMAGE_EXTENSION = "(.*?).(jpeg|jpg|png|gif)"; + public static final String README_IMAGE_FORMAT = "\\(([^)]*?%s[^)]*?)\\)"; + public static final String IMAGE_DOWNLOAD_URL_FORMAT = "(%s)"; + + public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService) { + this.gitHubService = gitHubService; + } + + @Override + public List convertProductJsonToMavenProductInfo(GHContent content) throws IOException { + List artifacts = new ArrayList<>(); + InputStream contentStream = extractedContentStream(content); + if (Objects.isNull(contentStream)) { + return artifacts; + } + + JsonNode rootNode = objectMapper.readTree(contentStream); + JsonNode installersNode = rootNode.path(ProductJsonConstants.INSTALLERS); + + for (JsonNode mavenNode : installersNode) { + JsonNode dataNode = mavenNode.path(ProductJsonConstants.DATA); + + // Not convert to artifact if id of node is not maven-import or maven-dependency + List installerIdsToDisplay = + List.of(ProductJsonConstants.MAVEN_DEPENDENCY_INSTALLER_ID, ProductJsonConstants.MAVEN_IMPORT_INSTALLER_ID); + if (!installerIdsToDisplay.contains(mavenNode.path(ProductJsonConstants.ID).asText())) { + continue; + } + + // Extract repository URL + JsonNode repositoriesNode = dataNode.path(ProductJsonConstants.REPOSITORIES); + repoUrl = repositoriesNode.get(0).path(ProductJsonConstants.URL).asText(); + + // Process projects + if (dataNode.has(ProductJsonConstants.PROJECTS)) { + extractMavenArtifactFromJsonNode(dataNode, false, artifacts); + } + + // Process dependencies + if (dataNode.has(ProductJsonConstants.DEPENDENCIES)) { + extractMavenArtifactFromJsonNode(dataNode, true, artifacts); + } + } + return artifacts; + } + + public InputStream extractedContentStream(GHContent content) { + try { + return content.read(); + } catch (IOException | NullPointerException e) { + log.warn("Can not read the current content: {}", e.getMessage()); + return null; + } + } + + public void extractMavenArtifactFromJsonNode(JsonNode dataNode, boolean isDependency, List artifacts) { + String nodeName = ProductJsonConstants.PROJECTS; + if (isDependency) { + nodeName = ProductJsonConstants.DEPENDENCIES; + } + JsonNode dependenciesNode = dataNode.path(nodeName); + for (JsonNode dependencyNode : dependenciesNode) { + MavenArtifact artifact = createArtifactFromJsonNode(dependencyNode, repoUrl, isDependency); + artifacts.add(artifact); + } + } + + public MavenArtifact createArtifactFromJsonNode(JsonNode node, String repoUrl, boolean isDependency) { + MavenArtifact artifact = new MavenArtifact(); + artifact.setRepoUrl(repoUrl); + artifact.setIsDependency(isDependency); + artifact.setGroupId(node.path(ProductJsonConstants.GROUP_ID).asText()); + artifact.setArtifactId(node.path(ProductJsonConstants.ARTIFACT_ID).asText()); + artifact.setType(node.path(ProductJsonConstants.TYPE).asText()); + artifact.setIsProductArtifact(true); + return artifact; + } + + @Override + public GHContent getContentFromGHRepoAndTag(String repoName, String filePath, String tagVersion) { + try { + return getOrganization().getRepository(repoName).getFileContent(filePath, tagVersion); + } catch (IOException e) { + log.error("Cannot Get Content From File Directory", e); + return null; + } + } + + public GHOrganization getOrganization() throws IOException { + if (organization == null) { + organization = gitHubService.getOrganization(GitHubConstants.AXONIVY_MARKET_ORGANIZATION_NAME); + } + return organization; + } + + @Override + public List getAllTagsFromRepoName(String repoName) throws IOException { + return getOrganization().getRepository(repoName).listTags().toList(); + } + + @Override + public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, GHRepository ghRepository, + String tag) { + ProductModuleContent productModuleContent = new ProductModuleContent(); + try { + List contents = getProductFolderContents(product, ghRepository, tag); + productModuleContent.setTag(tag); + getDependencyContentsFromProductJson(productModuleContent, contents); + GHContent readmeFile = contents.stream().filter(GHContent::isFile) + .filter(content -> ReadmeConstants.README_FILE.equals(content.getName())).findFirst().orElse(null); + if (Objects.nonNull(readmeFile)) { + String readmeContents = new String(readmeFile.read().readAllBytes()); + if (hasImageDirectives(readmeContents)) { + readmeContents = updateImagesWithDownloadUrl(product, contents, readmeContents); + } + getExtractedPartsOfReadme(productModuleContent, readmeContents); + } + } catch (Exception e) { + log.error("Cannot get product.json and README file's content {}", e); + return null; + } + return productModuleContent; + } + + private void getDependencyContentsFromProductJson(ProductModuleContent productModuleContent, List contents) + throws IOException { + GHContent productJsonFile = getProductJsonFile(contents); + if (Objects.nonNull(productJsonFile)) { + List artifacts = convertProductJsonToMavenProductInfo(productJsonFile); + MavenArtifact artifact = artifacts.stream().filter(MavenArtifact::getIsDependency).findFirst().orElse(null); + + if (Objects.nonNull(artifact)) { + productModuleContent.setIsDependency(Boolean.TRUE); + productModuleContent.setGroupId(artifact.getGroupId()); + productModuleContent.setArtifactId(artifact.getArtifactId()); + productModuleContent.setType(artifact.getType()); + productModuleContent.setName(artifact.getName()); + } + } + } + + private static GHContent getProductJsonFile(List contents) { + return contents.stream().filter(GHContent::isFile) + .filter(content -> ProductJsonConstants.PRODUCT_JSON_FILE.equals(content.getName())).findFirst().orElse(null); + } + + public String updateImagesWithDownloadUrl(Product product, List contents, String readmeContents) + throws IOException { + Map imageUrls = new HashMap<>(); + List productImages = contents.stream().filter(GHContent::isFile) + .filter(content -> content.getName().toLowerCase().matches(IMAGE_EXTENSION)).toList(); + if (!CollectionUtils.isEmpty(productImages)) { + for (GHContent productImage : productImages) { + imageUrls.put(productImage.getName(), productImage.getDownloadUrl()); + } + } else { + getImagesFromImageFolder(product, contents, imageUrls); + } + for (Map.Entry entry : imageUrls.entrySet()) { + String imageUrlPattern = String.format(README_IMAGE_FORMAT, Pattern.quote(entry.getKey())); + readmeContents = readmeContents.replaceAll(imageUrlPattern, String.format(IMAGE_DOWNLOAD_URL_FORMAT,entry.getValue())); + + } + return readmeContents; + } + + private void getImagesFromImageFolder(Product product, List contents, Map imageUrls) + throws IOException { + String imageFolderPath = GitHubUtils.getNonStandardImageFolder(product.getId()); + GHContent imageFolder = contents.stream().filter(GHContent::isDirectory) + .filter(content -> imageFolderPath.equals(content.getName())).findFirst().orElse(null); + if (Objects.nonNull(imageFolder)) { + for (GHContent imageContent : imageFolder.listDirectoryContent().toList()) { + imageUrls.put(imageContent.getName(), imageContent.getDownloadUrl()); + } + } + } + + // Cover some cases including when demo and setup parts switch positions or + // missing one of them + public void getExtractedPartsOfReadme(ProductModuleContent productModuleContent, String readmeContents) { + String[] parts = readmeContents.split(DEMO_SETUP_TITLE); + int demoIndex = readmeContents.indexOf(ReadmeConstants.DEMO_PART); + int setupIndex = readmeContents.indexOf(ReadmeConstants.SETUP_PART); + String description = Strings.EMPTY; + String setup = Strings.EMPTY; + String demo = Strings.EMPTY; + + if (parts.length > 0) { + description = removeFirstLine(parts[0]); + } + + if (demoIndex != -1 && setupIndex != -1) { + if (demoIndex < setupIndex) { + demo = parts[1]; + setup = parts[2]; + } else { + setup = parts[1]; + demo = parts[2]; + } + } else if (demoIndex != -1) { + demo = parts[1]; + } else if (setupIndex != -1) { + setup = parts[1]; + } + + productModuleContent.setDescription(description.trim()); + productModuleContent.setDemo(demo.trim()); + productModuleContent.setSetup(setup.trim()); + } + + private List getProductFolderContents(Product product, GHRepository ghRepository, String tag) + throws IOException { + String productFolderPath = ghRepository.getDirectoryContent(CommonConstants.SLASH, tag).stream() + .filter(GHContent::isDirectory).map(GHContent::getName) + .filter(content -> content.endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findFirst() + .orElse(null); + if (StringUtils.isBlank(productFolderPath) || hasChildConnector(ghRepository)) { + productFolderPath = GitHubUtils.getNonStandardProductFilePath(product.getId()); + } + + return ghRepository.getDirectoryContent(productFolderPath, tag); + } + + private boolean hasChildConnector(GHRepository ghRepository) { + return NonStandardProductPackageConstants.MICROSOFT_REPO_NAME.equals(ghRepository.getName()) + || NonStandardProductPackageConstants.OPENAI_CONNECTOR.equals(ghRepository.getName()); + } + + private boolean hasImageDirectives(String readmeContents) { + Pattern pattern = Pattern.compile(IMAGE_EXTENSION); + Matcher matcher = pattern.matcher(readmeContents); + return matcher.find(); + } + + private String removeFirstLine(String text) { + if (text.isBlank()) { + return Strings.EMPTY; + } + int index = text.indexOf(StringUtils.LF); + return index != -1 ? text.substring(index + 1).trim() : Strings.EMPTY; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java index 8d66b4649..4ccd7f1d4 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/util/GitHubUtils.java @@ -1,8 +1,13 @@ package com.axonivy.market.github.util; import java.io.IOException; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.PagedIterable; @@ -15,6 +20,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubUtils { + private static String pathToProductFolderFromTagContent; + private static String pathToImageFolder; + public static long getGHCommitDate(GHCommit commit) { long commitTime = 0l; if (commit != null) { @@ -46,4 +54,81 @@ public static List mapPagedIteratorToList(PagedIterable paged) { } return List.of(); } + + public static String convertArtifactIdToName(String artifactId) { + if (StringUtils.isBlank(artifactId)) { + return StringUtils.EMPTY; + } + return Arrays.stream(artifactId.split(CommonConstants.DASH_SEPARATOR)) + .map(part -> part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase()) + .collect(Collectors.joining(CommonConstants.SPACE_SEPARATOR)); + } + + public static String getNonStandardProductFilePath(String productId) { + switch (productId) { + case NonStandardProductPackageConstants.PORTAL: + pathToProductFolderFromTagContent = "AxonIvyPortal/portal-product"; + break; + case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: + pathToProductFolderFromTagContent = "connectivity/connectivity-demos-product"; + break; + case NonStandardProductPackageConstants.ERROR_HANDLING: + pathToProductFolderFromTagContent = "error-handling/error-handling-demos-product"; + break; + case NonStandardProductPackageConstants.WORKFLOW_DEMO: + pathToProductFolderFromTagContent = "workflow/workflow-demos-product"; + break; + case NonStandardProductPackageConstants.MICROSOFT_365: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-connector"; + break; + case NonStandardProductPackageConstants.MICROSOFT_CALENDAR: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-calendar"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TEAMS: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-chat"; + break; + case NonStandardProductPackageConstants.MICROSOFT_MAIL: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-mail"; + break; + case NonStandardProductPackageConstants.MICROSOFT_TODO: + pathToProductFolderFromTagContent = "msgraph-connector-product/products/msgraph-todo"; + break; + case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: + pathToProductFolderFromTagContent = "html-dialog/html-dialog-demos-product"; + break; + case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: + pathToProductFolderFromTagContent = "rule-engine/rule-engine-demos-product"; + break; + case NonStandardProductPackageConstants.OPENAI_CONNECTOR: + pathToProductFolderFromTagContent = "openai-connector-product"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToProductFolderFromTagContent = "openai-assistant-product"; + break; + default: + break; + } + return pathToProductFolderFromTagContent; + } + + public static String getNonStandardImageFolder(String productId) { + switch (productId) { + case NonStandardProductPackageConstants.EXCEL_IMPORTER: + pathToImageFolder = "doc"; + break; + case NonStandardProductPackageConstants.EXPRESS_IMPORTER, NonStandardProductPackageConstants.DEEPL_CONNECTOR: + pathToImageFolder = "img"; + break; + case NonStandardProductPackageConstants.GRAPHQL_DEMO: + pathToImageFolder = "assets"; + break; + case NonStandardProductPackageConstants.OPENAI_ASSISTANT: + pathToImageFolder = "docs"; + break; + default: + pathToImageFolder = "images"; + break; + } + return pathToImageFolder; + } } 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 new file mode 100644 index 000000000..2943ccc79 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -0,0 +1,38 @@ +package com.axonivy.market.model; + +import com.axonivy.market.entity.ProductModuleContent; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@Getter +@Setter +@NoArgsConstructor +public class ProductDetailModel extends ProductModel { + private String vendor; + private String platformReview; + private String newestReleaseVersion; + private String cost; + private String sourceUrl; + private String statusBadgeUrl; + private String language; + private String industry; + private String compatibility; + private Boolean contactUs; + private ProductModuleContent productModuleContent; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(getId()).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(getId(), ((ProductDetailModel) obj).getId()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 25ff5e794..7fabd79bc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -17,6 +17,8 @@ public interface ProductRepository extends MongoRepository { Product findByLogoUrl(String logoUrl); + Product findByIdAndType(String id, String type); + Optional findById(String productId); @Query("{'marketDirectory': {$regex : ?0, $options: 'i'}}") diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index 5e35368de..b90604d42 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -9,4 +9,8 @@ public interface ProductService { Page findProducts(String type, String keyword, String language, Pageable pageable); boolean syncLatestDataFromMarketRepo(); + + Product fetchProductDetail(String id); + + String getCompatibilityFromOldestTag(String oldestTag); } 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 70f7a3a16..80bfe0645 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 @@ -5,13 +5,18 @@ import java.io.IOException; import java.net.URL; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.entity.ProductModuleContent; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; @@ -26,16 +31,15 @@ import org.springframework.util.CollectionUtils; import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; import com.axonivy.market.enums.FileType; import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; import com.axonivy.market.factory.ProductFactory; import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.entity.GitHubRepoMeta; +import com.axonivy.market.enums.TypeOption; import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.ProductService; @@ -48,16 +52,21 @@ public class ProductServiceImpl implements ProductService { private final ProductRepository productRepository; private final GHAxonIvyMarketRepoService axonIvyMarketRepoService; + private final GHAxonIvyProductRepoService axonIvyProductRepoService; private final GitHubRepoMetaRepository gitHubRepoMetaRepository; private final GitHubService gitHubService; private GHCommit lastGHCommit; private GitHubRepoMeta marketRepoMeta; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, - GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { + GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, + GitHubService gitHubService) { this.productRepository = productRepository; this.axonIvyMarketRepoService = axonIvyMarketRepoService; + this.axonIvyProductRepoService = axonIvyProductRepoService; this.gitHubRepoMetaRepository = gitHubRepoMetaRepository; this.gitHubService = gitHubService; } @@ -68,22 +77,22 @@ public Page findProducts(String type, String keyword, String language, final var searchPageable = refinePagination(language, pageable); Page result = Page.empty(); switch (typeOption) { - case ALL: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findAll(searchPageable); - } else { - result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); - } - break; - case CONNECTORS, UTILITIES, SOLUTIONS: - if (StringUtils.isBlank(keyword)) { - result = productRepository.findByType(typeOption.getCode(), searchPageable); - } else { - result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); - } - break; - default: - break; + case ALL: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findAll(searchPageable); + } else { + result = productRepository.searchByNameOrShortDescriptionRegex(keyword, language, searchPageable); + } + break; + case CONNECTORS, UTILITIES, SOLUTIONS: + if (StringUtils.isBlank(keyword)) { + result = productRepository.findByType(typeOption.getCode(), searchPageable); + } else { + result = productRepository.searchByKeywordAndType(keyword, typeOption.getCode(), language, searchPageable); + } + break; + default: + break; } return result; } @@ -107,8 +116,8 @@ private void syncRepoMetaDataStatus() { if (lastGHCommit == null) { return; } - String repoURL = Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath) - .orElse(EMPTY); + String repoURL = + Optional.ofNullable(lastGHCommit.getOwner()).map(GHRepository::getUrl).map(URL::getPath).orElse(EMPTY); marketRepoMeta.setRepoURL(repoURL); marketRepoMeta.setRepoName(GitHubConstants.AXONIVY_MARKETPLACE_REPO_NAME); marketRepoMeta.setLastSHA1(lastGHCommit.getSHA1()); @@ -144,7 +153,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { } ProductFactory.mappingByGHContent(product, fileContent); - updateLatestReleaseDateForProduct(product); + updateProductFromReleaseTags(product); if (FileType.META == file.getType()) { modifyProductByMetaContent(file, product); } else { @@ -157,34 +166,34 @@ private void updateLatestChangeToProductsFromGithubRepo() { private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { Product result = null; switch (file.getStatus()) { - case MODIFIED, ADDED: - result = productRepository.findByMarketDirectoryRegex(parentPath); - if (result != null) { - result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); - productRepository.save(result); - } - break; - case REMOVED: - result = productRepository.findByLogoUrl(product.getLogoUrl()); - if (result != null) { - productRepository.deleteById(result.getId()); - } - break; - default: - break; + case MODIFIED, ADDED: + result = productRepository.findByMarketDirectoryRegex(parentPath); + if (result != null) { + result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); + productRepository.save(result); + } + break; + case REMOVED: + result = productRepository.findByLogoUrl(product.getLogoUrl()); + if (result != null) { + productRepository.deleteById(result.getId()); + } + break; + default: + break; } } private void modifyProductByMetaContent(GitHubFile file, Product product) { switch (file.getStatus()) { - case MODIFIED, ADDED: - productRepository.save(product); - break; - case REMOVED: - productRepository.deleteById(product.getId()); - break; - default: - break; + case MODIFIED, ADDED: + productRepository.save(product); + break; + case REMOVED: + productRepository.deleteById(product.getId()); + break; + default: + break; } } @@ -225,7 +234,7 @@ private Page syncProductsFromGitHubRepo() { Product product = new Product(); for (var content : ghContentEntity.getValue()) { ProductFactory.mappingByGHContent(product, content); - updateLatestReleaseDateForProduct(product); + updateProductFromReleaseTags(product); } products.add(product); }); @@ -235,17 +244,64 @@ private Page syncProductsFromGitHubRepo() { return new PageImpl<>(products); } - private void updateLatestReleaseDateForProduct(Product product) { + private void updateProductFromReleaseTags(Product product) { if (StringUtils.isBlank(product.getRepositoryName())) { return; } try { GHRepository productRepo = gitHubService.getRepository(product.getRepositoryName()); - GHTag lastTag = CollectionUtils.firstElement(productRepo.listTags().toList()); - product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); - product.setNewestReleaseVersion(lastTag.getName()); + List tags = productRepo.listTags().toList(); + GHTag lastTag = CollectionUtils.firstElement(tags); + if (lastTag != null) { + product.setNewestPublishedDate(lastTag.getCommit().getCommitDate()); + product.setNewestReleaseVersion(lastTag.getName()); + } + + String oldestTag = tags.stream().map(tag -> tag.getName().replaceAll(NON_NUMERIC_CHAR, Strings.EMPTY)).distinct() + .sorted(Comparator.reverseOrder()).reduce((tag1, tag2) -> tag2).orElse(null); + if (oldestTag != null && StringUtils.isBlank(product.getCompatibility())) { + String compatibility = getCompatibilityFromOldestTag(oldestTag); + product.setCompatibility(compatibility); + } + + List> completableFutures = new ArrayList<>(); + ExecutorService service = Executors.newFixedThreadPool(10); + for (GHTag ghtag : tags) { + completableFutures.add(CompletableFuture.supplyAsync( + () -> axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()), + service)); + } + completableFutures.forEach(CompletableFuture::join); + List productModuleContents = completableFutures.stream().map(completableFuture -> { + try { + return completableFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + log.error("Get readme and product json contents failed", e); + return null; + } + }).toList(); + product.setProductModuleContents(productModuleContents); } catch (Exception e) { log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); } } + + // Cover 3 cases after removing non-numeric characters (8, 11.1 and 10.0.2) + public String getCompatibilityFromOldestTag(String oldestTag) { + if (!oldestTag.contains(CommonConstants.DOT_SEPARATOR)) { + return oldestTag + ".0+"; + } + int firstDot = oldestTag.indexOf(CommonConstants.DOT_SEPARATOR); + int secondDot = oldestTag.indexOf(CommonConstants.DOT_SEPARATOR, firstDot + 1); + if (secondDot == -1) { + return oldestTag.concat(CommonConstants.PLUS); + } + return oldestTag.substring(0, secondDot).concat(CommonConstants.PLUS); + } + + @Override + public Product fetchProductDetail(String id) { + return productRepository.findById(id).orElse(null); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java index 36988dcea..06d07123d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/VersionServiceImpl.java @@ -1,5 +1,6 @@ package com.axonivy.market.service.impl; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.NonStandardProductPackageConstants; @@ -9,13 +10,14 @@ import com.axonivy.market.github.model.MavenArtifact; import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.VersionService; import com.axonivy.market.comparator.ArchivedArtifactsComparator; import com.axonivy.market.comparator.LatestVersionComparator; -import com.axonivy.market.utils.XmlReaderUtils; +import com.axonivy.market.util.XmlReaderUtils; import lombok.Getter; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.BooleanUtils; @@ -26,7 +28,6 @@ import java.io.IOException; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; @Log4j2 @@ -34,317 +35,285 @@ @Getter public class VersionServiceImpl implements VersionService { - private final GHAxonIvyProductRepoService gitHubService; - private final MavenArtifactVersionRepository mavenArtifactVersionRepository; - private final ProductRepository productRepository; - @Getter - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; - private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); - @Getter - private String productJsonFilePath; - private String productId; - - public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, - MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository) { - this.gitHubService = gitHubService; - this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; - this.productRepository = productRepository; - - } - - private void resetData() { - repoName = null; - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = Collections.emptyList(); - proceedDataCache = null; - metaProductArtifact = null; - productJsonFilePath = null; - productId = null; - - } - - public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, - String designerVersion) { - List results = new ArrayList<>(); - resetData(); - - this.productId = productId; - artifactsFromMeta = getProductMetaArtifacts(productId); - List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); - proceedDataCache = mavenArtifactVersionRepository.findById(productId) - .orElse(new MavenArtifactVersion(productId)); - metaProductArtifact = artifactsFromMeta.stream() - .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)) - .findAny().orElse(new MavenArtifact()); - - sanitizeMetaArtifactBeforeHandle(); - - boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); - if (isNewVersionDetected) { - mavenArtifactVersionRepository.save(proceedDataCache); - } - return results; - } - - public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, - List result) { - boolean isNewVersionDetected = false; - for (String version : versionsToDisplay) { - List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); - List productArtifactModels = proceedDataCache.getProductArtifactWithVersionReleased() - .get(version); - if (productArtifactModels == null) { - isNewVersionDetected = true; - productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); - } - artifactsInVersion.addAll(productArtifactModels); - result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); - } - return isNewVersionDetected; - } - - public List updateArtifactsInVersionWithProductArtifact(String version) { - List productArtifactModels = convertMavenArtifactsToModels(getProductJsonByVersion(version), - version); - proceedDataCache.getVersions().add(version); - proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); - return productArtifactModels; - } - - public List getProductMetaArtifacts(String productId) { - Product productInfo = productRepository.findById(productId).orElse(new Product()); - String fullRepoName = productInfo.getRepositoryName(); - if (StringUtils.isNotEmpty(fullRepoName)) { - repoName = getRepoNameFromMarketRepo(fullRepoName); - } - return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); - } - - public void sanitizeMetaArtifactBeforeHandle() { - artifactsFromMeta.remove(metaProductArtifact); - artifactsFromMeta.forEach(artifact -> { - List archivedArtifacts = new ArrayList<>( - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()).stream() - .sorted(new ArchivedArtifactsComparator()).toList()); - Collections.reverse(archivedArtifacts); - archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); - }); - } - - @Override - public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { - List versions = getVersionsFromMavenArtifacts(); - Stream versionStream = versions.stream(); - if (BooleanUtils.isTrue(isShowDevVersion)) { - return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) - .sorted(new LatestVersionComparator()).toList(); - } - if (StringUtils.isNotBlank(designerVersion)) { - return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); - } - return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); - } - - public List getVersionsFromMavenArtifacts() { - Set versions = new HashSet<>(); - for (MavenArtifact artifact : artifactsFromMeta) { - versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), - artifact.getArtifactId())); - Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) - .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), - archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); - } - List versionList = new ArrayList<>(versions); - versionList.sort(new LatestVersionComparator()); - return versionList; - } - - @Override - public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { - List versions = new ArrayList<>(); - String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); - if (StringUtils.isNotBlank(baseUrl)) { - versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); - } - return versions; - } - - @Override - public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { - if (StringUtils.isAnyBlank(groupId, artifactID)) { - return StringUtils.EMPTY; - } - repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - groupId = groupId.replace(MavenConstants.DOT_SEPARATOR, MavenConstants.GROUP_ID_URL_SEPARATOR); - return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); - } - - public String getBugfixVersion(String version) { - - if (isSnapshotVersion(version)) { - version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); - } else if (isSprintVersion(version)) { - version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; - } - String[] segments = version.split("\\."); - if (segments.length >= 3) { - segments[2] = segments[2].split(MavenConstants.ARTIFACT_ID_SEPARATOR)[0]; - return segments[0] + MavenConstants.DOT_SEPARATOR + segments[1] + MavenConstants.DOT_SEPARATOR - + segments[2]; - } - return version; - } - - public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { - if (isReleasedVersion(version)) { - return true; - } - String bugfixVersion; - if (isSnapshotVersion(version)) { - bugfixVersion = getBugfixVersion( - version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); - } else { - bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); - } - return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) && isReleasedVersion(currentVersion) - && getBugfixVersion(currentVersion).equals(bugfixVersion)); - } - - public boolean isSnapshotVersion(String version) { - return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); - } - - public boolean isSprintVersion(String version) { - return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); - } - - public boolean isReleasedVersion(String version) { - return !(isSprintVersion(version) || isSnapshotVersion(version)); - } - - public boolean isMatchWithDesignerVersion(String version, String designerVersion) { - return isReleasedVersion(version) && version.startsWith(designerVersion); - } - - public List getProductJsonByVersion(String version) { - List result = new ArrayList<>(); - String versionTag = buildProductJsonFilePath(version); - try { - GHContent productJsonContent = gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, - versionTag); - if (Objects.isNull(productJsonContent)) { - return result; - } - result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); - } catch (IOException e) { - log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, - productJsonFilePath, versionTag); - } - return result; - } - - public String buildProductJsonFilePath(String version) { - String versionTag = "v" + version; - String pathToProductJsonFileFromTagContent = metaProductArtifact.getArtifactId(); - switch (productId) { - case NonStandardProductPackageConstants.PORTAL: - pathToProductJsonFileFromTagContent = "AxonIvyPortal/portal-product"; - versionTag = version; - break; - case NonStandardProductPackageConstants.CONNECTIVITY_FEATURE: - pathToProductJsonFileFromTagContent = "connectivity/connectivity-demos-product"; - break; - case NonStandardProductPackageConstants.ERROR_HANDLING: - pathToProductJsonFileFromTagContent = "error-handling/error-handling-demos-product"; - break; - case NonStandardProductPackageConstants.WORKFLOW_DEMO: - pathToProductJsonFileFromTagContent = "workflow/workflow-demos-product"; - break; - case NonStandardProductPackageConstants.MICROSOFT_365: - pathToProductJsonFileFromTagContent = "msgraph-connector-product/products/msgraph-connector"; - break; - case NonStandardProductPackageConstants.HTML_DIALOG_DEMO: - pathToProductJsonFileFromTagContent = "html-dialog/html-dialog-demos-product"; - break; - case NonStandardProductPackageConstants.RULE_ENGINE_DEMOS: - pathToProductJsonFileFromTagContent = "rule-engine/rule-engine-demos-product"; - break; - default: - break; - } - productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, - pathToProductJsonFileFromTagContent); - return versionTag; - } - - public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { - String artifactName = artifact.getName(); - if (StringUtils.isBlank(artifactName)) { - artifactName = convertArtifactIdToName(artifact.getArtifactId()); - } - artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); - artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); - return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), - artifact.getIsProductArtifact()); - } - - public List convertMavenArtifactsToModels(List artifacts, String version) { - List results = new ArrayList<>(); - if (!CollectionUtils.isEmpty(artifacts)) { - for (MavenArtifact artifact : artifacts) { - MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); - results.add(mavenArtifactModel); - } - } - return results; - } - - public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { - String groupIdByVersion = artifact.getGroupId(); - String artifactIdByVersion = artifact.getArtifactId(); - String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); - ArchivedArtifact archivedArtifactBestMatchVersion = findArchivedArtifactInfoBestMatchWithVersion( - artifact.getArtifactId(), version); - - if (Objects.nonNull(archivedArtifactBestMatchVersion)) { - groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); - artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); - } - groupIdByVersion = groupIdByVersion.replace(MavenConstants.DOT_SEPARATOR, - MavenConstants.GROUP_ID_URL_SEPARATOR); - return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, - artifactIdByVersion, version, artifactIdByVersion, version, artifact.getType()); - } - - public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { - List archivedArtifacts = archivedArtifactsMap.get(artifactId); - - if (CollectionUtils.isEmpty(archivedArtifacts)) { - return null; - } - for (ArchivedArtifact archivedArtifact : archivedArtifacts) { - if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { - return archivedArtifact; - } - } - return null; - } - - public String convertArtifactIdToName(String artifactId) { - if (StringUtils.isBlank(artifactId)) { - return StringUtils.EMPTY; - } - return Arrays.stream(artifactId.split(MavenConstants.ARTIFACT_ID_SEPARATOR)) - .map(part -> part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase()) - .collect(Collectors.joining(MavenConstants.ARTIFACT_NAME_SEPARATOR)); - } - - public String getRepoNameFromMarketRepo(String fullRepoName) { - String[] repoNamePart = fullRepoName.split("/"); - return repoNamePart[repoNamePart.length - 1]; - } -} \ No newline at end of file + private final GHAxonIvyProductRepoService gitHubService; + private final MavenArtifactVersionRepository mavenArtifactVersionRepository; + private final ProductRepository productRepository; + @Getter + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + private final LatestVersionComparator latestVersionComparator = new LatestVersionComparator(); + @Getter + private String productJsonFilePath; + private String productId; + + public VersionServiceImpl(GHAxonIvyProductRepoService gitHubService, + MavenArtifactVersionRepository mavenArtifactVersionRepository, ProductRepository productRepository) { + this.gitHubService = gitHubService; + this.mavenArtifactVersionRepository = mavenArtifactVersionRepository; + this.productRepository = productRepository; + + } + + private void resetData() { + repoName = null; + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = Collections.emptyList(); + proceedDataCache = null; + metaProductArtifact = null; + productJsonFilePath = null; + productId = null; + } + + public List getArtifactsAndVersionToDisplay(String productId, Boolean isShowDevVersion, + String designerVersion) { + List results = new ArrayList<>(); + resetData(); + + this.productId = productId; + artifactsFromMeta = getProductMetaArtifacts(productId); + List versionsToDisplay = getVersionsToDisplay(isShowDevVersion, designerVersion); + proceedDataCache = mavenArtifactVersionRepository.findById(productId).orElse(new MavenArtifactVersion(productId)); + metaProductArtifact = artifactsFromMeta.stream() + .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findAny() + .orElse(new MavenArtifact()); + + sanitizeMetaArtifactBeforeHandle(); + + boolean isNewVersionDetected = handleArtifactForVersionToDisplay(versionsToDisplay, results); + if (isNewVersionDetected) { + mavenArtifactVersionRepository.save(proceedDataCache); + } + return results; + } + + public boolean handleArtifactForVersionToDisplay(List versionsToDisplay, + List result) { + boolean isNewVersionDetected = false; + for (String version : versionsToDisplay) { + List artifactsInVersion = convertMavenArtifactsToModels(artifactsFromMeta, version); + List productArtifactModels = + proceedDataCache.getProductArtifactWithVersionReleased().get(version); + if (productArtifactModels == null) { + isNewVersionDetected = true; + productArtifactModels = updateArtifactsInVersionWithProductArtifact(version); + } + artifactsInVersion.addAll(productArtifactModels); + result.add(new MavenArtifactVersionModel(version, artifactsInVersion.stream().distinct().toList())); + } + return isNewVersionDetected; + } + + public List updateArtifactsInVersionWithProductArtifact(String version) { + List productArtifactModels = + convertMavenArtifactsToModels(getProductJsonByVersion(version), version); + proceedDataCache.getVersions().add(version); + proceedDataCache.getProductArtifactWithVersionReleased().put(version, productArtifactModels); + return productArtifactModels; + } + + public List getProductMetaArtifacts(String productId) { + Product productInfo = productRepository.findById(productId).orElse(new Product()); + String fullRepoName = productInfo.getRepositoryName(); + if (StringUtils.isNotEmpty(fullRepoName)) { + repoName = getRepoNameFromMarketRepo(fullRepoName); + } + return Optional.ofNullable(productInfo.getArtifacts()).orElse(new ArrayList<>()); + } + + public void sanitizeMetaArtifactBeforeHandle() { + artifactsFromMeta.remove(metaProductArtifact); + artifactsFromMeta.forEach(artifact -> { + List archivedArtifacts = new ArrayList<>(Optional.ofNullable(artifact.getArchivedArtifacts()) + .orElse(Collections.emptyList()).stream().sorted(new ArchivedArtifactsComparator()).toList()); + Collections.reverse(archivedArtifacts); + archivedArtifactsMap.put(artifact.getArtifactId(), archivedArtifacts); + }); + } + + @Override + public List getVersionsToDisplay(Boolean isShowDevVersion, String designerVersion) { + List versions = getVersionsFromMavenArtifacts(); + Stream versionStream = versions.stream(); + if (BooleanUtils.isTrue(isShowDevVersion)) { + return versionStream.filter(version -> isOfficialVersionOrUnReleasedDevVersion(versions, version)) + .sorted(new LatestVersionComparator()).toList(); + } + if (StringUtils.isNotBlank(designerVersion)) { + return versionStream.filter(version -> isMatchWithDesignerVersion(version, designerVersion)).toList(); + } + return versions.stream().filter(this::isReleasedVersion).sorted(new LatestVersionComparator()).toList(); + } + + public List getVersionsFromMavenArtifacts() { + Set versions = new HashSet<>(); + for (MavenArtifact artifact : artifactsFromMeta) { + versions.addAll( + getVersionsFromArtifactDetails(artifact.getRepoUrl(), artifact.getGroupId(), artifact.getArtifactId())); + Optional.ofNullable(artifact.getArchivedArtifacts()).orElse(Collections.emptyList()) + .forEach(archivedArtifact -> versions.addAll(getVersionsFromArtifactDetails(artifact.getRepoUrl(), + archivedArtifact.getGroupId(), archivedArtifact.getArtifactId()))); + } + List versionList = new ArrayList<>(versions); + versionList.sort(new LatestVersionComparator()); + return versionList; + } + + @Override + public List getVersionsFromArtifactDetails(String repoUrl, String groupId, String artifactID) { + List versions = new ArrayList<>(); + String baseUrl = buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactID); + if (StringUtils.isNotBlank(baseUrl)) { + versions.addAll(XmlReaderUtils.readXMLFromUrl(baseUrl)); + } + return versions; + } + + @Override + public String buildMavenMetadataUrlFromArtifact(String repoUrl, String groupId, String artifactID) { + if (StringUtils.isAnyBlank(groupId, artifactID)) { + return StringUtils.EMPTY; + } + repoUrl = Optional.ofNullable(repoUrl).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + groupId = groupId.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.format(MavenConstants.METADATA_URL_FORMAT, repoUrl, groupId, artifactID); + } + + public String getBugfixVersion(String version) { + + if (isSnapshotVersion(version)) { + version = version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY); + } else if (isSprintVersion(version)) { + version = version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]; + } + String[] segments = version.split("\\."); + if (segments.length >= 3) { + segments[2] = segments[2].split(CommonConstants.DASH_SEPARATOR)[0]; + return segments[0] + CommonConstants.DOT_SEPARATOR + segments[1] + CommonConstants.DOT_SEPARATOR + segments[2]; + } + return version; + } + + public boolean isOfficialVersionOrUnReleasedDevVersion(List versions, String version) { + if (isReleasedVersion(version)) { + return true; + } + String bugfixVersion; + if (isSnapshotVersion(version)) { + bugfixVersion = getBugfixVersion(version.replace(MavenConstants.SNAPSHOT_RELEASE_POSTFIX, StringUtils.EMPTY)); + } else { + bugfixVersion = getBugfixVersion(version.split(MavenConstants.SPRINT_RELEASE_POSTFIX)[0]); + } + return versions.stream().noneMatch(currentVersion -> !currentVersion.equals(version) + && isReleasedVersion(currentVersion) && getBugfixVersion(currentVersion).equals(bugfixVersion)); + } + + public boolean isSnapshotVersion(String version) { + return version.endsWith(MavenConstants.SNAPSHOT_RELEASE_POSTFIX); + } + + public boolean isSprintVersion(String version) { + return version.contains(MavenConstants.SPRINT_RELEASE_POSTFIX); + } + + public boolean isReleasedVersion(String version) { + return !(isSprintVersion(version) || isSnapshotVersion(version)); + } + + public boolean isMatchWithDesignerVersion(String version, String designerVersion) { + return isReleasedVersion(version) && version.startsWith(designerVersion); + } + + public List getProductJsonByVersion(String version) { + List result = new ArrayList<>(); + String versionTag = getVersionTag(version); + productJsonFilePath = buildProductJsonFilePath(); + try { + GHContent productJsonContent = + gitHubService.getContentFromGHRepoAndTag(repoName, productJsonFilePath, versionTag); + if (Objects.isNull(productJsonContent)) { + return result; + } + result = gitHubService.convertProductJsonToMavenProductInfo(productJsonContent); + } catch (IOException e) { + log.warn("Can not get the product.json from repo {} by path in {} version {}", repoName, productJsonFilePath, + versionTag); + } + return result; + } + + public String getVersionTag(String version) { + String versionTag = "v" + version; + if (NonStandardProductPackageConstants.PORTAL.equals(productId)) { + versionTag = version; + } + return versionTag; + } + + public String buildProductJsonFilePath() { + String pathToProductFolderFromTagContent = metaProductArtifact.getArtifactId(); + GitHubUtils.getNonStandardProductFilePath(productId); + productJsonFilePath = + String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, pathToProductFolderFromTagContent); + return productJsonFilePath; + } + + public MavenArtifactModel convertMavenArtifactToModel(MavenArtifact artifact, String version) { + String artifactName = artifact.getName(); + if (StringUtils.isBlank(artifactName)) { + artifactName = GitHubUtils.convertArtifactIdToName(artifact.getArtifactId()); + } + artifact.setType(Optional.ofNullable(artifact.getType()).orElse("iar")); + artifactName = String.format(MavenConstants.ARTIFACT_NAME_FORMAT, artifactName, artifact.getType()); + return new MavenArtifactModel(artifactName, buildDownloadUrlFromArtifactAndVersion(artifact, version), + artifact.getIsProductArtifact()); + } + + public List convertMavenArtifactsToModels(List artifacts, String version) { + List results = new ArrayList<>(); + if (!CollectionUtils.isEmpty(artifacts)) { + for (MavenArtifact artifact : artifacts) { + MavenArtifactModel mavenArtifactModel = convertMavenArtifactToModel(artifact, version); + results.add(mavenArtifactModel); + } + } + return results; + } + + public String buildDownloadUrlFromArtifactAndVersion(MavenArtifact artifact, String version) { + String groupIdByVersion = artifact.getGroupId(); + String artifactIdByVersion = artifact.getArtifactId(); + String repoUrl = Optional.ofNullable(artifact.getRepoUrl()).orElse(MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL); + ArchivedArtifact archivedArtifactBestMatchVersion = + findArchivedArtifactInfoBestMatchWithVersion(artifact.getArtifactId(), version); + + if (Objects.nonNull(archivedArtifactBestMatchVersion)) { + groupIdByVersion = archivedArtifactBestMatchVersion.getGroupId(); + artifactIdByVersion = archivedArtifactBestMatchVersion.getArtifactId(); + } + groupIdByVersion = groupIdByVersion.replace(CommonConstants.DOT_SEPARATOR, CommonConstants.SLASH); + return String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, repoUrl, groupIdByVersion, artifactIdByVersion, + version, artifactIdByVersion, version, artifact.getType()); + } + + public ArchivedArtifact findArchivedArtifactInfoBestMatchWithVersion(String artifactId, String version) { + List archivedArtifacts = archivedArtifactsMap.get(artifactId); + + if (CollectionUtils.isEmpty(archivedArtifacts)) { + return null; + } + for (ArchivedArtifact archivedArtifact : archivedArtifacts) { + if (latestVersionComparator.compare(archivedArtifact.getLastVersion(), version) <= 0) { + return archivedArtifact; + } + } + return null; + } + + public String getRepoNameFromMarketRepo(String fullRepoName) { + String[] repoNamePart = fullRepoName.split("/"); + return repoNamePart[repoNamePart.length - 1]; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java new file mode 100644 index 000000000..d49802145 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/XmlReaderUtils.java @@ -0,0 +1,58 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.MavenConstants; +import lombok.extern.log4j.Log4j2; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Log4j2 +public class XmlReaderUtils { + private static final RestTemplate restTemplate = new RestTemplate(); + + private XmlReaderUtils() {} + + public static List readXMLFromUrl(String url) { + List versions = new ArrayList<>(); + try { + String xmlData = restTemplate.getForObject(url, String.class); + extractVersions(xmlData, versions); + } catch (HttpClientErrorException e) { + log.error(e.getMessage()); + } + return versions; + } + + public static void extractVersions(String xmlData, List versions) { + try { + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xmlData))); + + XPath xpath = XPathFactory.newInstance().newXPath(); + XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); + + Object result = expr.evaluate(document, XPathConstants.NODESET); + NodeList versionNodes = (NodeList) result; + + for (int i = 0; i < versionNodes.getLength(); i++) { + versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java b/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java deleted file mode 100644 index 8f7f6a6bc..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/utils/XmlReaderUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.axonivy.market.utils; - -import com.axonivy.market.constants.MavenConstants; -import lombok.extern.log4j.Log4j2; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathFactory; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Log4j2 -public class XmlReaderUtils { - private static final RestTemplate restTemplate = new RestTemplate(); - - private XmlReaderUtils() { - } - - public static List readXMLFromUrl(String url) { - List versions = new ArrayList<>(); - try { - String xmlData = restTemplate.getForObject(url, String.class); - extractVersions(xmlData, versions); - } catch (HttpClientErrorException e) { - log.error(e.getMessage()); - } - return versions; - } - - public static void extractVersions(String xmlData, List versions) { - try { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document document = builder.parse(new InputSource(new StringReader(xmlData))); - - XPath xpath = XPathFactory.newInstance().newXPath(); - XPathExpression expr = xpath.compile(MavenConstants.VERSION_EXTRACT_FORMAT_FROM_METADATA_FILE); - - Object result = expr.evaluate(document, XPathConstants.NODESET); - NodeList versionNodes = (NodeList) result; - - for (int i = 0; i < versionNodes.getLength(); i++) { - versions.add(Optional.ofNullable(versionNodes.item(i)).map(Node::getTextContent).orElse(null)); - } - } catch (Exception e) { - log.error(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 1779befd8..814044478 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -1,13 +1,17 @@ package com.axonivy.market.controller; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.axonivy.market.model.MavenArtifactVersionModel; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.service.VersionService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; + import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -17,28 +21,81 @@ import java.util.List; import java.util.Objects; +import com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.service.ProductService; + @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { - - @InjectMocks - private ProductDetailsController productDetailsController; + @Mock + private ProductService productService; @Mock VersionService versionService; + @Mock + private ProductDetailModelAssembler detailModelAssembler; + + @InjectMocks + private ProductDetailsController productDetailsController; + private static final String PRODUCT_NAME_SAMPLE = "Docker"; + private static final String PRODUCT_NAME_DE_SAMPLE = "Docker DE"; + @Test - void testFindProduct() { - var result = productDetailsController.findProduct("", ""); - assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode()); + void testProductDetails() { + Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), null)).thenReturn(createProductMockWithDetails()); + ResponseEntity mockExpectedResult = + new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + + ResponseEntity result = productDetailsController.findProductDetails("docker-connector"); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(result, mockExpectedResult); + + verify(productService, times(1)).fetchProductDetail("docker-connector"); + verify(detailModelAssembler, times(1)).toModel(mockProduct(), null); } @Test - void testFindProductVersionsById(){ + void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); - Mockito.when(versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())).thenReturn(models); - ResponseEntity> result = productDetailsController.findProductVersionsById("protal", true, "10.0.1"); + Mockito.when( + versionService.getArtifactsAndVersionToDisplay(Mockito.anyString(), Mockito.anyBoolean(), Mockito.anyString())) + .thenReturn(models); + ResponseEntity> result = + productDetailsController.findProductVersionsById("protal", true, "10.0.1"); Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()); Assertions.assertEquals(1, Objects.requireNonNull(result.getBody()).size()); Assertions.assertEquals(models, result.getBody()); } -} \ No newline at end of file + + private Product mockProduct() { + Product mockProduct = new Product(); + mockProduct.setId("docker-connector"); + MultilingualismValue name = new MultilingualismValue(); + name.setEn(PRODUCT_NAME_SAMPLE); + name.setDe(PRODUCT_NAME_DE_SAMPLE); + mockProduct.setNames(name); + mockProduct.setLanguage("English"); + return mockProduct; + } + + private ProductDetailModel createProductMockWithDetails() { + ProductDetailModel mockProductDetail = new ProductDetailModel(); + mockProductDetail.setId("docker-connector"); + MultilingualismValue name = new MultilingualismValue(); + name.setEn(PRODUCT_NAME_SAMPLE); + name.setDe(PRODUCT_NAME_DE_SAMPLE); + mockProductDetail.setNames(name); + mockProductDetail.setType("connector"); + mockProductDetail.setCompatibility("10.0+"); + mockProductDetail.setSourceUrl("https://github.com/axonivy-market/docker-connector"); + mockProductDetail.setStatusBadgeUrl("https://github.com/axonivy-market/docker-connector"); + mockProductDetail.setLanguage("English"); + mockProductDetail.setIndustry("Cross-Industry"); + mockProductDetail.setContactUs(false); + return mockProductDetail; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java index 0221c4ce6..dd770b752 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/factory/ProductFactoryTest.java @@ -1,7 +1,7 @@ package com.axonivy.market.factory; -import static com.axonivy.market.constants.CommonConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.mock; @@ -31,7 +31,7 @@ void testMappingByGHContent() throws IOException { GHContent mockContent = mock(GHContent.class); var result = ProductFactory.mappingByGHContent(product, null); assertEquals(product, result); - when(mockContent.getName()).thenReturn(CommonConstants.META_FILE); + when(mockContent.getName()).thenReturn(META_FILE); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(mockContent.read()).thenReturn(inputStream); result = ProductFactory.mappingByGHContent(product, mockContent); diff --git a/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java deleted file mode 100644 index 28293329d..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/github/service/GHAxonIvyProductRepoServiceImplTest.java +++ /dev/null @@ -1,242 +0,0 @@ -package com.axonivy.market.github.service; - -import com.axonivy.market.constants.ProductJsonConstants; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.*; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class GHAxonIvyProductRepoServiceImplTest { - - private static final String DUMMY_TAG = "v1.0.0"; - - @Mock - PagedIterable listTags; - - @Mock - GHRepository ghRepository; - - @Mock - GitHubService githubService; - - @Mock - GHOrganization organization; - - @Mock - JsonNode dataNode; - - @Mock - JsonNode childNode; - - @Mock - GHContent content = new GHContent(); - - @InjectMocks - @Spy - private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; - - void setup() throws IOException { - var mockGHOrganization = mock(GHOrganization.class); - when(githubService.getOrganization(any())).thenReturn(mockGHOrganization); - when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); - } - - @Test - void testAllTagsFromRepoName() throws IOException { - setup(); - var mockTag = mock(GHTag.class); - when(mockTag.getName()).thenReturn(DUMMY_TAG); - when(listTags.toList()).thenReturn(List.of(mockTag)); - when(ghRepository.listTags()).thenReturn(listTags); - var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); - assertEquals(1, result.size()); - assertEquals(DUMMY_TAG, result.get(0).getName()); - } - - @Test - void testContentFromGHRepoAndTag() throws IOException { - setup(); - var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); - assertNull(result); - when(axonivyProductRepoServiceImpl.getOrganization()).thenThrow(IOException.class); - result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); - assertNull(result); - } - - @Test - void testExtractMavenArtifactFromJsonNode() { - List artifacts = new ArrayList<>(); - boolean isDependency = true; - String nodeName = ProductJsonConstants.DEPENDENCIES; - - createListNodeForDataNoteByName(nodeName); - MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); - - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); - - assertEquals(1, artifacts.size()); - assertSame(mockArtifact, artifacts.get(0)); - - isDependency = false; - nodeName = ProductJsonConstants.PROJECTS; - createListNodeForDataNoteByName(nodeName); - - Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, - isDependency); - - axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); - - assertEquals(2, artifacts.size()); - assertSame(mockArtifact, artifacts.get(1)); - } - - private void createListNodeForDataNoteByName(String nodeName) { - JsonNode sectionNode = Mockito.mock(JsonNode.class); - Iterator iterator = Mockito.mock(Iterator.class); - Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); - Mockito.when(sectionNode.iterator()).thenReturn(iterator); - Mockito.when(iterator.hasNext()).thenReturn(true, false); - Mockito.when(iterator.next()).thenReturn(childNode); - } - - @Test - void testCreateArtifactFromJsonNode() { - String repoUrl = "http://example.com/repo"; - boolean isDependency = true; - String groupId = "com.example"; - String artifactId = "example-artifact"; - String type = "jar"; - - JsonNode groupIdNode = Mockito.mock(JsonNode.class); - JsonNode artifactIdNode = Mockito.mock(JsonNode.class); - JsonNode typeNode = Mockito.mock(JsonNode.class); - Mockito.when(groupIdNode.asText()).thenReturn(groupId); - Mockito.when(artifactIdNode.asText()).thenReturn(artifactId); - Mockito.when(typeNode.asText()).thenReturn(type); - Mockito.when(dataNode.path(ProductJsonConstants.GROUP_ID)).thenReturn(groupIdNode); - Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); - Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); - - MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, - isDependency); - - assertEquals(repoUrl, artifact.getRepoUrl()); - assertTrue(artifact.getIsDependency()); - assertEquals(groupId, artifact.getGroupId()); - assertEquals(artifactId, artifact.getArtifactId()); - assertEquals(type, artifact.getType()); - assertTrue(artifact.getIsProductArtifact()); - } - - @Test - void testConvertProductJsonToMavenProductInfo() throws IOException { - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - - InputStream inputStream = getMockInputStream(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - inputStream = getMockInputStreamWithOutProjectAndDependency(); - Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); - assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); - } - - private static InputStream getMockInputStream() { - String jsonContent = """ - { - "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", - "installers": [ - { - "id": "maven-import", - "data": { - "projects": [ - { - "groupId": "com.axonivy.utils.bpmnstatistic", - "artifactId": "bpmn-statistic-demo", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - }, - { - "id": "maven-dependency", - "data": { - "dependencies": [ - { - "groupId": "com.axonivy.utils.bpmnstatistic", - "artifactId": "bpmn-statistic", - "version": "${version}", - "type": "iar" - } - ], - "repositories": [ - { - "id": "maven.axonivy.com", - "url": "https://maven.axonivy.com", - "snapshots": { - "enabled": "true" - } - } - ] - } - } - ] - } - """; - return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); - } - - private static InputStream getMockInputStreamWithOutProjectAndDependency() { - String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" - + " \"repositories\": [\n" + " {\n" - + " \"url\": \"http://example.com/repo\"\n" + " }\n" + " ]\n" + " }\n" - + " }\n" + " ]\n" + "}"; - return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); - } - - @Test - void testExtractedContentStream() { - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); - assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); - } - - @Test - void testGetOrganization() throws IOException { - Mockito.when(githubService.getOrganization(Mockito.anyString())).thenReturn(organization); - assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); - assertEquals(organization, axonivyProductRepoServiceImpl.getOrganization()); - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java new file mode 100644 index 000000000..fa0104d4a --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyProductRepoServiceImplTest.java @@ -0,0 +1,375 @@ +package com.axonivy.market.service; + +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GHAxonIvyProductRepoServiceImplTest { + + private static final String DUMMY_TAG = "v1.0.0"; + public static final String RELEASE_TAG = "v10.0.0"; + public static final String IMAGE_NAME = "image.png"; + public static final String DOCUWARE_CONNECTOR_PRODUCT = "docuware-connector-product"; + public static final String IMAGE_DOWNLOAD_URL = "https://raw.githubusercontent.com/image.png"; + + @Mock + PagedIterable listTags; + + @Mock + GHRepository ghRepository; + + @Mock + GitHubService gitHubService; + + GHOrganization mockGHOrganization = mock(GHOrganization.class); + + @Mock + JsonNode dataNode; + + @Mock + JsonNode childNode; + + @Mock + GHContent content = new GHContent(); + + @InjectMocks + @Spy + private GHAxonIvyProductRepoServiceImpl axonivyProductRepoServiceImpl; + + void setup() throws IOException { + when(gitHubService.getOrganization(any())).thenReturn(mockGHOrganization); + when(mockGHOrganization.getRepository(any())).thenReturn(ghRepository); + } + + @AfterEach + void after() throws IOException { + reset(mockGHOrganization); + reset(gitHubService); + } + + @Test + void testAllTagsFromRepoName() throws IOException { + setup(); + var mockTag = mock(GHTag.class); + when(mockTag.getName()).thenReturn(DUMMY_TAG); + when(listTags.toList()).thenReturn(List.of(mockTag)); + when(ghRepository.listTags()).thenReturn(listTags); + var result = axonivyProductRepoServiceImpl.getAllTagsFromRepoName(""); + assertEquals(1, result.size()); + assertEquals(DUMMY_TAG, result.get(0).getName()); + } + + @Test + void testContentFromGHRepoAndTag() throws IOException { + setup(); + var result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + when(axonivyProductRepoServiceImpl.getOrganization()).thenThrow(IOException.class); + result = axonivyProductRepoServiceImpl.getContentFromGHRepoAndTag("", null, null); + assertNull(result); + } + + @Test + void testExtractMavenArtifactFromJsonNode() { + List artifacts = new ArrayList<>(); + boolean isDependency = true; + String nodeName = ProductJsonConstants.DEPENDENCIES; + + createListNodeForDataNoteByName(nodeName); + MavenArtifact mockArtifact = Mockito.mock(MavenArtifact.class); + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(1, artifacts.size()); + assertSame(mockArtifact, artifacts.get(0)); + + isDependency = false; + nodeName = ProductJsonConstants.PROJECTS; + createListNodeForDataNoteByName(nodeName); + + Mockito.doReturn(mockArtifact).when(axonivyProductRepoServiceImpl).createArtifactFromJsonNode(childNode, null, + isDependency); + + axonivyProductRepoServiceImpl.extractMavenArtifactFromJsonNode(dataNode, isDependency, artifacts); + + assertEquals(2, artifacts.size()); + assertSame(mockArtifact, artifacts.get(1)); + } + + private void createListNodeForDataNoteByName(String nodeName) { + JsonNode sectionNode = Mockito.mock(JsonNode.class); + Iterator iterator = Mockito.mock(Iterator.class); + Mockito.when(dataNode.path(nodeName)).thenReturn(sectionNode); + Mockito.when(sectionNode.iterator()).thenReturn(iterator); + Mockito.when(iterator.hasNext()).thenReturn(true, false); + Mockito.when(iterator.next()).thenReturn(childNode); + } + + @Test + void testCreateArtifactFromJsonNode() { + String repoUrl = "http://example.com/repo"; + boolean isDependency = true; + String groupId = "com.example"; + String artifactId = "example-artifact"; + String type = "jar"; + + JsonNode groupIdNode = Mockito.mock(JsonNode.class); + JsonNode artifactIdNode = Mockito.mock(JsonNode.class); + JsonNode typeNode = Mockito.mock(JsonNode.class); + Mockito.when(groupIdNode.asText()).thenReturn(groupId); + Mockito.when(artifactIdNode.asText()).thenReturn(artifactId); + Mockito.when(typeNode.asText()).thenReturn(type); + Mockito.when(dataNode.path(ProductJsonConstants.GROUP_ID)).thenReturn(groupIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.ARTIFACT_ID)).thenReturn(artifactIdNode); + Mockito.when(dataNode.path(ProductJsonConstants.TYPE)).thenReturn(typeNode); + + MavenArtifact artifact = axonivyProductRepoServiceImpl.createArtifactFromJsonNode(dataNode, repoUrl, isDependency); + + assertEquals(repoUrl, artifact.getRepoUrl()); + assertTrue(artifact.getIsDependency()); + assertEquals(groupId, artifact.getGroupId()); + assertEquals(artifactId, artifact.getArtifactId()); + assertEquals(type, artifact.getType()); + assertTrue(artifact.getIsProductArtifact()); + } + + @Test + void testConvertProductJsonToMavenProductInfo() throws IOException { + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(null).size()); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + + InputStream inputStream = getMockInputStream(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(2, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + inputStream = getMockInputStreamWithOutProjectAndDependency(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(content)).thenReturn(inputStream); + assertEquals(0, axonivyProductRepoServiceImpl.convertProductJsonToMavenProductInfo(content).size()); + } + + @Test + void testExtractedContentStream() { + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(null)); + assertNull(axonivyProductRepoServiceImpl.extractedContentStream(content)); + } + + @Test + void testGetOrganization() throws IOException { + Mockito.when(gitHubService.getOrganization(Mockito.anyString())).thenReturn(mockGHOrganization); + assertEquals(mockGHOrganization, axonivyProductRepoServiceImpl.getOrganization()); + assertEquals(mockGHOrganization, axonivyProductRepoServiceImpl.getOrganization()); + } + + @Test + void testGetReadmeAndProductContentsFromTag() throws IOException { + String readmeContentWithImage = + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + + GHContent mockContent = createMockProductFolderWithProductJson(); + + getReadmeInputStream(readmeContentWithImage, mockContent); + InputStream inputStream = getMockInputStream(); + Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(any())).thenReturn(inputStream); + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + + assertEquals(RELEASE_TAG, result.getTag()); + assertTrue(result.getIsDependency()); + assertEquals("com.axonivy.utils.bpmnstatistic", result.getGroupId()); + assertEquals("bpmn-statistic", result.getArtifactId()); + assertEquals("iar", result.getType()); + assertEquals("Test README", result.getDescription()); + assertEquals("Demo content", result.getDemo()); + assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup()); + } + + @Test + void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException { + String readmeContentWithImageFolder = + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (./images/image.png)"; + + GHContent mockImageFile = mock(GHContent.class); + when(mockImageFile.getName()).thenReturn(ReadmeConstants.IMAGES, IMAGE_NAME); + when(mockImageFile.isDirectory()).thenReturn(true); + when(mockImageFile.getDownloadUrl()).thenReturn(IMAGE_DOWNLOAD_URL); + + PagedIterable pagedIterable = mock(PagedIterable.class); + when(mockImageFile.listDirectoryContent()).thenReturn(pagedIterable); + when(pagedIterable.toList()).thenReturn(List.of(mockImageFile)); + + String updatedReadme = axonivyProductRepoServiceImpl.updateImagesWithDownloadUrl(createMockProduct(), + List.of(mockImageFile), readmeContentWithImageFolder); + + assertEquals( + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (https://raw.githubusercontent.com/image.png)", + updatedReadme); + } + + @Test + void testGetReadmeAndProductContentsFromTag_WithNoFullyThreeParts() throws IOException { + String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content"; + + GHContent mockContent = createMockProductFolder(); + + getReadmeInputStream(readmeContentString, mockContent); + + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + + assertNull(result.getArtifactId()); + assertEquals("Setup content", result.getSetup()); + } + + @Test + void testGetReadmeAndProductContentsFromTag_SwitchPartsPosition() throws IOException { + String readmeContentString = "#Product-name\n Test README\n## Setup\nSetup content\n## Demo\nDemo content"; + + GHContent mockContent = createMockProductFolder(); + + getReadmeInputStream(readmeContentString, mockContent); + + var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, + RELEASE_TAG); + assertEquals("Demo content", result.getDemo()); + assertEquals("Setup content", result.getSetup()); + } + + private static void getReadmeInputStream(String readmeContentString, GHContent mockContent) throws IOException { + InputStream mockReadmeInputStream = mock(InputStream.class); + when(mockContent.read()).thenReturn(mockReadmeInputStream); + when(mockReadmeInputStream.readAllBytes()).thenReturn(readmeContentString.getBytes()); + } + + private static InputStream getMockInputStream() { + String jsonContent = """ + { + "$schema": "https://json-schema.axonivy.com/market/10.0.0/product.json", + "installers": [ + { + "id": "maven-import", + "data": { + "projects": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic-demo", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + }, + { + "id": "maven-dependency", + "data": { + "dependencies": [ + { + "groupId": "com.axonivy.utils.bpmnstatistic", + "artifactId": "bpmn-statistic", + "version": "${version}", + "type": "iar" + } + ], + "repositories": [ + { + "id": "maven.axonivy.com", + "url": "https://maven.axonivy.com", + "snapshots": { + "enabled": "true" + } + } + ] + } + } + ] + } + """; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + private static InputStream getMockInputStreamWithOutProjectAndDependency() { + String jsonContent = "{\n" + " \"installers\": [\n" + " {\n" + " \"data\": {\n" + + " \"repositories\": [\n" + " {\n" + " \"url\": \"http://example.com/repo\"\n" + + " }\n" + " ]\n" + " }\n" + " }\n" + " ]\n" + "}"; + return new ByteArrayInputStream(jsonContent.getBytes(StandardCharsets.UTF_8)); + } + + private Product createMockProduct() throws IOException { + Product product = new Product(); + product.setId("docuware-connector"); + product.setLanguage("en"); + return product; + } + + private GHContent createMockProductFolder() throws IOException { + GHContent mockContent = mock(GHContent.class); + when(mockContent.isDirectory()).thenReturn(true); + when(mockContent.isFile()).thenReturn(true); + when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); + + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)).thenReturn(List.of(mockContent)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)).thenReturn(List.of(mockContent)); + + return mockContent; + } + + private GHContent createMockProductFolderWithProductJson() throws IOException { + GHContent mockContent = mock(GHContent.class); + when(mockContent.isDirectory()).thenReturn(true); + when(mockContent.isFile()).thenReturn(true); + when(mockContent.getName()).thenReturn(DOCUWARE_CONNECTOR_PRODUCT, ReadmeConstants.README_FILE); + + GHContent mockContent2 = createMockProductJson(); + + when(ghRepository.getDirectoryContent(CommonConstants.SLASH, RELEASE_TAG)) + .thenReturn(List.of(mockContent, mockContent2)); + when(ghRepository.getDirectoryContent(DOCUWARE_CONNECTOR_PRODUCT, RELEASE_TAG)) + .thenReturn(List.of(mockContent, mockContent2)); + + return mockContent; + } + + private static GHContent createMockProductJson() throws IOException { + GHContent mockProductJson = mock(GHContent.class); + when(mockProductJson.isFile()).thenReturn(true); + when(mockProductJson.getName()).thenReturn(ProductJsonConstants.PRODUCT_JSON_FILE, IMAGE_NAME); + when(mockProductJson.getDownloadUrl()).thenReturn(IMAGE_DOWNLOAD_URL); + return mockProductJson; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java index bcebfd4e8..9fd472991 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/ProductServiceImplTest.java @@ -1,32 +1,30 @@ package com.axonivy.market.service; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.META_FILE; +import static com.axonivy.market.constants.MetaConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; +import com.axonivy.market.model.MultilingualismValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHContent; +import org.kohsuke.github.*; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -46,7 +44,6 @@ import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.repository.GitHubRepoMetaRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.ProductServiceImpl; @@ -57,13 +54,17 @@ class ProductServiceImplTest { private static final String SAMPLE_PRODUCT_ID = "amazon-comprehend"; private static final String SAMPLE_PRODUCT_NAME = "Amazon Comprehend"; private static final long LAST_CHANGE_TIME = 1718096290000l; - private static final Pageable PAGEABLE = PageRequest.of(0, 20, - Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); + private static final Pageable PAGEABLE = + PageRequest.of(0, 20, Sort.by(SortOption.ALPHABETICALLY.getOption()).descending()); private static final String SHA1_SAMPLE = "35baa89091b2452b77705da227f1a964ecabc6c8"; + public static final String RELEASE_TAG = "v10.0.2"; private String keyword; private String langague; private Page mockResultReturn; + @Mock + private GHRepository ghRepository; + @Mock private ProductRepository productRepository; @@ -76,6 +77,12 @@ class ProductServiceImplTest { @Mock private GitHubService gitHubService; + @Mock + private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; + + @Captor + ArgumentCaptor> argumentCaptor; + @InjectMocks private ProductServiceImpl productService; @@ -188,9 +195,14 @@ void testFindAllProductsWithKeyword() throws IOException { assertEquals(SAMPLE_PRODUCT_NAME, result.getContent().get(0).getNames().getEn()); // Test has keyword and type is connector - when(productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))).thenReturn( - new PageImpl<>(mockResultReturn.stream().filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) - && product.getType().equals(TypeOption.CONNECTORS.getCode())).collect(Collectors.toList()))); + when( + productRepository.searchByKeywordAndType(any(), any(), any(), any(Pageable.class))) + .thenReturn( + new PageImpl<>( + mockResultReturn.stream() + .filter(product -> product.getNames().getEn().equals(SAMPLE_PRODUCT_NAME) + && product.getType().equals(TypeOption.CONNECTORS.getCode())) + .collect(Collectors.toList()))); // Executes result = productService.findProducts(TypeOption.CONNECTORS.getOption(), SAMPLE_PRODUCT_NAME, langague, PAGEABLE); assertTrue(result.hasContent()); @@ -202,6 +214,20 @@ void testSyncProductsFirstTime() throws IOException { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); + when(ghAxonIvyProductRepoService.getReadmeAndProductContentsFromTag(any(), any(), anyString())) + .thenReturn(mockReadmeProductContent()); + when(gitHubService.getRepository(any())).thenReturn(ghRepository); + PagedIterable pagedIterable = mock(PagedIterable.class); + when(ghRepository.listTags()).thenReturn(pagedIterable); + + GHTag mockTag = mock(GHTag.class); + GHCommit mockGHCommit = mock(GHCommit.class); + + when(mockTag.getName()).thenReturn(RELEASE_TAG); + when(mockTag.getCommit()).thenReturn(mockGHCommit); + when(mockGHCommit.getCommitDate()).thenReturn(new Date()); + + when(pagedIterable.toList()).thenReturn(List.of(mockTag)); var mockContent = mockGHContentAsMetaJSON(); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); @@ -212,8 +238,12 @@ void testSyncProductsFirstTime() throws IOException { when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); // Executes - var result = productService.syncLatestDataFromMarketRepo(); - assertEquals(false, result); + productService.syncLatestDataFromMarketRepo(); + + verify(productRepository).saveAll(argumentCaptor.capture()); + + assertThat(argumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() + .isEqualTo(List.of(mockReadmeProductContent())); } @Test @@ -235,13 +265,37 @@ void testSearchProducts() { String type = TypeOption.ALL.getOption(); keyword = "on"; langague = "en"; - when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)).thenReturn(mockResultReturn); + when(productRepository.searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable)) + .thenReturn(mockResultReturn); var result = productService.findProducts(type, keyword, langague, simplePageable); assertEquals(result, mockResultReturn); verify(productRepository).searchByNameOrShortDescriptionRegex(keyword, langague, simplePageable); } + @Test + void testFetchProductDetail() { + String id = "amazon-comprehend"; + Product mockProduct = mockResultReturn.getContent().get(0); + when(productRepository.findById(id)).thenReturn(Optional.ofNullable(mockProduct)); + Product result = productService.fetchProductDetail(id); + assertEquals(mockProduct, result); + verify(productRepository, times(1)).findById(id); + } + + @Test + void testGetCompatibilityFromNumericTag() { + + String result = productService.getCompatibilityFromOldestTag("1.0.0"); + assertEquals("1.0+", result); + + result = productService.getCompatibilityFromOldestTag("8"); + assertEquals("8.0+", result); + + result = productService.getCompatibilityFromOldestTag("11.2"); + assertEquals("11.2+", result); + } + private Page createPageProductsMock() { var mockProducts = new ArrayList(); MultilingualismValue name = new MultilingualismValue(); @@ -282,4 +336,12 @@ private GHContent mockGHContentAsMetaJSON() { when(mockGHContent.getName()).thenReturn(META_FILE); return mockGHContent; } + + private ProductModuleContent mockReadmeProductContent() { + ProductModuleContent productModuleContent = new ProductModuleContent(); + productModuleContent.setTag("v10.0.2"); + productModuleContent.setName("Amazon Comprehend"); + productModuleContent.setDescription("testDescription"); + return productModuleContent; + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java index f9f75b439..ce3ef83ee 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/VersionServiceImplTest.java @@ -1,33 +1,6 @@ package com.axonivy.market.service; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Fail; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.kohsuke.github.GHContent; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - import com.axonivy.market.constants.MavenConstants; -import com.axonivy.market.constants.NonStandardProductPackageConstants; import com.axonivy.market.entity.MavenArtifactModel; import com.axonivy.market.entity.MavenArtifactVersion; import com.axonivy.market.entity.Product; @@ -38,535 +11,486 @@ import com.axonivy.market.repository.MavenArtifactVersionRepository; import com.axonivy.market.repository.ProductRepository; import com.axonivy.market.service.impl.VersionServiceImpl; -import com.axonivy.market.utils.XmlReaderUtils; +import com.axonivy.market.util.XmlReaderUtils; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Fail; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.*; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.util.*; @ExtendWith(MockitoExtension.class) class VersionServiceImplTest { - private String repoName; - private Map> archivedArtifactsMap; - private List artifactsFromMeta; - private MavenArtifactVersion proceedDataCache; - private MavenArtifact metaProductArtifact; - @Spy - @InjectMocks - private VersionServiceImpl versionService; - - @Mock - private GHAxonIvyProductRepoService gitHubService; - - @Mock - private MavenArtifactVersionRepository mavenArtifactVersionRepository; - - @Mock - private ProductRepository productRepository; - - @BeforeEach() - void prepareBeforeTest() { - archivedArtifactsMap = new HashMap<>(); - artifactsFromMeta = new ArrayList<>(); - metaProductArtifact = new MavenArtifact(); - proceedDataCache = new MavenArtifactVersion(); - repoName = StringUtils.EMPTY; - ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); - ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); - ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); - ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); - } - - private void setUpArtifactFromMeta() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - metaProductArtifact.setGroupId(groupId); - metaProductArtifact.setArtifactId(artifactId); - metaProductArtifact.setIsProductArtifact(true); - MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, - null); - artifactsFromMeta.add(metaProductArtifact); - artifactsFromMeta.add(additionalMavenArtifact); - } - - @Test - void testGetArtifactsAndVersionToDisplay() { - String productId = "adobe-acrobat-sign-connector"; - String targetVersion = "10.0.10"; - setUpArtifactFromMeta(); - when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); - when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) - .thenReturn(List.of(targetVersion)); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); - Assertions.assertEquals(1, - versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); - - MavenArtifactVersion proceededData = new MavenArtifactVersion(); - proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); - when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); - Assertions.assertEquals(1, - versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); - } - - @Test - void testHandleArtifactForVersionToDisplay() { - String newVersionDetected = "10.0.10"; - List result = new ArrayList<>(); - List versionsToDisplay = List.of(newVersionDetected); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); - Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); - - result = new ArrayList<>(); - ArrayList artifactsInVersion = new ArrayList<>(); - artifactsInVersion.add(new MavenArtifactModel()); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(artifactsInVersion); - Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); - } - - @Test - void testGetProductMetaArtifacts() { - Product product = new Product(); - MavenArtifact artifact1 = new MavenArtifact(); - MavenArtifact artifact2 = new MavenArtifact(); - List artifacts = List.of(artifact1, artifact2); - product.setArtifacts(artifacts); - when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); - List result = versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals(artifacts, result); - Assertions.assertNull(versionService.getRepoName()); - - product.setRepositoryName("/market/portal"); - versionService.getProductMetaArtifacts("portal"); - Assertions.assertEquals("portal", versionService.getRepoName()); - } - - @Test - void testUpdateArtifactsInVersionWithProductArtifact() { - String version = "10.0.10"; - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifactModel artifactModel = new MavenArtifactModel(); - List mockMavenArtifactModels = List.of(artifactModel); - when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); - when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) - .thenReturn(mockMavenArtifactModels); - Assertions.assertEquals(mockMavenArtifactModels, - versionService.updateArtifactsInVersionWithProductArtifact(version)); - Assertions.assertEquals(1, proceedDataCache.getVersions().size()); - Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); - Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); - } - - @Test - void testSanitizeMetaArtifactBeforeHandle() { - setUpArtifactFromMeta(); - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String archivedArtifactId1 = "adobe-acrobat-sign-connector"; - String archivedArtifactId2 = "adobe-acrobat-sign-connector"; - ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); - ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); - artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); - - versionService.sanitizeMetaArtifactBeforeHandle(); - String artifactId = "adobe-acrobat-sign-connector"; - - Assertions.assertEquals(1, artifactsFromMeta.size()); - Assertions.assertEquals(1, archivedArtifactsMap.size()); - Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); - Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); - } - - @Test - void testGetVersionsToDisplay() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - versionFromArtifact.add("10.0.3-SNAPSHOT"); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) - .thenReturn(versionFromArtifact); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); - Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); - versionFromArtifact.remove("10.0.3-SNAPSHOT"); - Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); - } - - @Test - void getVersionsFromMavenArtifacts() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String archivedArtifactId = "adobe-sign-connector"; - artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.6"); - versionFromArtifact.add("10.0.5"); - versionFromArtifact.add("10.0.4"); - - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)) - .thenReturn(versionFromArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - - List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); - ArrayList versionFromArchivedArtifact = new ArrayList<>(); - versionFromArchivedArtifact.add("10.0.3"); - versionFromArchivedArtifact.add("10.0.2"); - versionFromArchivedArtifact.add("10.0.1"); - artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); - when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) - .thenReturn(versionFromArchivedArtifact); - versionFromArtifact.addAll(versionFromArchivedArtifact); - Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); - } - - @Test - void testGetVersionsFromArtifactDetails() { - - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - - ArrayList versionFromArtifact = new ArrayList<>(); - versionFromArtifact.add("10.0.16"); - versionFromArtifact.add("10.0.18"); - versionFromArtifact.add("10.0.19"); - versionFromArtifact.add("10.0.20"); - versionFromArtifact.add("10.0.21"); - - try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { - xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); - } - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); - Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), - versionFromArtifact); - } - - @Test - void testBuildMavenMetadataUrlFromArtifact() { - String repoUrl = "https://maven.axonivy.com"; - String groupId = "com.axonivy.connector.adobe.acrobat.sign"; - String artifactId = "adobe-acrobat-sign-connector"; - String metadataUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; - Assertions.assertEquals(StringUtils.EMPTY, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); - Assertions.assertEquals(StringUtils.EMPTY, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), StringUtils.EMPTY); - Assertions.assertEquals(metadataUrl, - versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); - } - - @Test - void testIsReleasedVersionOrUnReleaseDevVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - String unreleasedSprintVersion = "10.0.21-m1235"; - List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); - Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); - Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); - Assertions - .assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); - } - - @Test - void testGetBugfixVersion() { - String releasedVersion = "10.0.20"; - String snapshotVersion = "10.0.20-SNAPSHOT"; - String sprintVersion = "10.0.20-m1234"; - String minorSprintVersion = "10.0.20.1-m1234"; - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); - Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); - } - - @Test - void testIsSnapshotVersion() { - String targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21-m1234"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); - } - - @Test - void testIsSprintVersion() { - String targetVersion = "10.0.21-m1234"; - Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - - targetVersion = "10.0.21"; - Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); - } - - @Test - void testIsReleasedVersion() { - String targetVersion = "10.0.21"; - Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - - targetVersion = "10.0.21-m1231"; - Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); - } - - @Test - void testIsMatchWithDesignerVersion() { - String designerVersion = "10.0.21"; - String targetVersion = "10.0.21.2"; - Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.21-SNAPSHOT"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - - targetVersion = "10.0.19"; - Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); - } - - @Test - void testGetProductJsonByVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector.adobe.acrobat"; - GHContent mockContent = mock(GHContent.class); - repoName = "adobe-acrobat-sign-connector"; - ReflectionTestUtils.setField(versionService, "repoName", repoName); - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - MavenArtifact productArtifact = new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, - targetArtifactId, "iar", null, true, null); - - metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); - metaProductArtifact.setGroupId(targetGroupId); - metaProductArtifact.setArtifactId(targetArtifactId); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(null); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - - metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); - when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) - .thenReturn(mockContent); - - try { - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); - Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); - - when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) - .thenThrow(new IOException("Mock IO Exception")); - Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); - } catch (IOException e) { - Fail.fail("Mock setup should not throw an exception"); - } - } - - @Test - void testConvertMavenArtifactToModel() { - String downloadUrl = "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; - String artifactName = "Adobe Acrobat Sign Connector (iar)"; - - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - - // Assert case handle artifact without name - MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedResult.getName(), result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - - // Assert case handle artifact with name - artifactName = "Adobe Connector"; - String expectedArtifactName = "Adobe Connector (iar)"; - targetArtifact.setName(artifactName); - result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); - expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); - Assertions.assertEquals(expectedArtifactName, result.getName()); - Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); - } - - @Test - void testConvertMavenArtifactsToModels() { - // Assert case param is empty - List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), - "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is null - result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); - Assertions.assertEquals(Collections.emptyList(), result); - - // Assert case param is a list with existed element - MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", - "adobe-acrobat-sign-connector", null, null, null, null); - result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); - Assertions.assertEquals(1, result.size()); - } - - @Test - void testBuildDownloadUrlFromArtifactAndVersion() { - // Set up artifact for testing - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetGroupId = "com.axonivy.connector"; - MavenArtifact targetArtifact = new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, - null); - String targetVersion = "10.0.10"; - - // Assert case without archived artifact - String expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, - MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, "com/axonivy/connector", targetArtifactId, targetVersion, - targetArtifactId, targetVersion, "iar"); - String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact not match & use custom repo - ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", - "adobe-connector"); - ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", - "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); - archivedArtifactsMap.put(targetArtifactId, - List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); - String customRepoUrl = "https://nexus.axonivy.com"; - targetArtifact.setRepoUrl(customRepoUrl); - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, - "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); - Assertions.assertEquals(expectedResult, result); - - // Assert case with artifact got matching archived artifact & use custom file - // type - String customType = "zip"; - targetArtifact.setType(customType); - targetVersion = "10.0.9"; - result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); - expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, - "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, - customType); - Assertions.assertEquals(expectedResult, result); - } - - @Test - void testFindArchivedArtifactInfoBestMatchWithVersion() { - String targetArtifactId = "adobe-acrobat-sign-connector"; - String targetVersion = "10.0.10"; - ArchivedArtifact result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, - targetVersion); - Assertions.assertNull(result); - - // Assert case with target version higher than all of latest version from - // archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion8 = new ArchivedArtifact("10.0.8", "com.axonivy.connector", - "adobe-sign-connector"); - ArchivedArtifact adobeArchivedArtifactVersion9 = new ArchivedArtifact("10.0.9", "com.axonivy.connector", - "adobe-acrobat-sign-connector"); - List archivedArtifacts = new ArrayList<>(); - archivedArtifacts.add(adobeArchivedArtifactVersion8); - archivedArtifacts.add(adobeArchivedArtifactVersion9); - archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); - Assertions.assertNull(result); - - // Assert case with target version less than all of latest version from archived - // artifact list - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); - Assertions.assertEquals(adobeArchivedArtifactVersion8, result); - - // Assert case with target version is in range of archived artifact list - ArchivedArtifact adobeArchivedArtifactVersion10 = new ArchivedArtifact("10.0.10", "com.axonivy.connector", - "adobe-sign-connector"); - - archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); - result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); - Assertions.assertEquals(adobeArchivedArtifactVersion10, result); - } - - @Test - void testConvertArtifactIdToName() { - String defaultArtifactId = "adobe-acrobat-sign-connector"; - String result = versionService.convertArtifactIdToName(defaultArtifactId); - Assertions.assertEquals("Adobe Acrobat Sign Connector", result); - - result = versionService.convertArtifactIdToName(null); - Assertions.assertEquals(StringUtils.EMPTY, result); - - result = versionService.convertArtifactIdToName(StringUtils.EMPTY); - Assertions.assertEquals(StringUtils.EMPTY, result); - - result = versionService.convertArtifactIdToName(" "); - Assertions.assertEquals(StringUtils.EMPTY, result); - } - - @Test - void testGetRepoNameFromMarketRepo() { - String defaultRepositoryName = "market/adobe-acrobat-connector"; - String expectedRepoName = "adobe-acrobat-connector"; - String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "market/utils/adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - - defaultRepositoryName = "adobe-acrobat-connector"; - result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); - Assertions.assertEquals(expectedRepoName, result); - } - - @Test - void testBuildProductJsonFilePath() { - String version = "10.0.1"; - ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); - Assertions.assertEquals("v10.0.1", versionService.buildProductJsonFilePath(version)); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.PORTAL); - Assertions.assertEquals("10.0.1", versionService.buildProductJsonFilePath(version)); - Assertions.assertEquals("AxonIvyPortal/portal-product/product.json", versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", - NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("connectivity/connectivity-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.ERROR_HANDLING); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("error-handling/error-handling-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.WORKFLOW_DEMO); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("workflow/workflow-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.MICROSOFT_365); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.HTML_DIALOG_DEMO); - versionService.buildProductJsonFilePath(version); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("html-dialog/html-dialog-demos-product/product.json", - versionService.getProductJsonFilePath()); - - ReflectionTestUtils.setField(versionService, "productId", NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); - versionService.buildProductJsonFilePath(version); - Assertions.assertEquals("rule-engine/rule-engine-demos-product/product.json", - versionService.getProductJsonFilePath()); - } -} \ No newline at end of file + private String repoName; + private Map> archivedArtifactsMap; + private List artifactsFromMeta; + private MavenArtifactVersion proceedDataCache; + private MavenArtifact metaProductArtifact; + @Spy + @InjectMocks + private VersionServiceImpl versionService; + + @Mock + private GHAxonIvyProductRepoService gitHubService; + + @Mock + private MavenArtifactVersionRepository mavenArtifactVersionRepository; + + @Mock + private ProductRepository productRepository; + + @BeforeEach() + void prepareBeforeTest() { + archivedArtifactsMap = new HashMap<>(); + artifactsFromMeta = new ArrayList<>(); + metaProductArtifact = new MavenArtifact(); + proceedDataCache = new MavenArtifactVersion(); + repoName = StringUtils.EMPTY; + ReflectionTestUtils.setField(versionService, "archivedArtifactsMap", archivedArtifactsMap); + ReflectionTestUtils.setField(versionService, "artifactsFromMeta", artifactsFromMeta); + ReflectionTestUtils.setField(versionService, "proceedDataCache", proceedDataCache); + ReflectionTestUtils.setField(versionService, "metaProductArtifact", metaProductArtifact); + } + + private void setUpArtifactFromMeta() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + metaProductArtifact.setGroupId(groupId); + metaProductArtifact.setArtifactId(artifactId); + metaProductArtifact.setIsProductArtifact(true); + MavenArtifact additionalMavenArtifact = new MavenArtifact(repoUrl, "", groupId, artifactId, "", null, null, null); + artifactsFromMeta.add(metaProductArtifact); + artifactsFromMeta.add(additionalMavenArtifact); + } + + @Test + void testGetArtifactsAndVersionToDisplay() { + String productId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + setUpArtifactFromMeta(); + when(versionService.getProductMetaArtifacts(Mockito.anyString())).thenReturn(artifactsFromMeta); + when(versionService.getVersionsToDisplay(Mockito.anyBoolean(), Mockito.anyString())) + .thenReturn(List.of(targetVersion)); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.empty()); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + + MavenArtifactVersion proceededData = new MavenArtifactVersion(); + proceededData.getProductArtifactWithVersionReleased().put(targetVersion, new ArrayList<>()); + when(mavenArtifactVersionRepository.findById(Mockito.anyString())).thenReturn(Optional.of(proceededData)); + Assertions.assertEquals(1, versionService.getArtifactsAndVersionToDisplay(productId, false, targetVersion).size()); + } + + @Test + void testHandleArtifactForVersionToDisplay() { + String newVersionDetected = "10.0.10"; + List result = new ArrayList<>(); + List versionsToDisplay = List.of(newVersionDetected); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + Assertions.assertTrue(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(newVersionDetected, result.get(0).getVersion()); + Assertions.assertEquals(0, result.get(0).getArtifactsByVersion().size()); + + result = new ArrayList<>(); + ArrayList artifactsInVersion = new ArrayList<>(); + artifactsInVersion.add(new MavenArtifactModel()); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(artifactsInVersion); + Assertions.assertFalse(versionService.handleArtifactForVersionToDisplay(versionsToDisplay, result)); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(1, result.get(0).getArtifactsByVersion().size()); + } + + @Test + void testGetProductMetaArtifacts() { + Product product = new Product(); + MavenArtifact artifact1 = new MavenArtifact(); + MavenArtifact artifact2 = new MavenArtifact(); + List artifacts = List.of(artifact1, artifact2); + product.setArtifacts(artifacts); + when(productRepository.findById(Mockito.anyString())).thenReturn(Optional.of(product)); + List result = versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals(artifacts, result); + Assertions.assertNull(versionService.getRepoName()); + + product.setRepositoryName("/market/portal"); + versionService.getProductMetaArtifacts("portal"); + Assertions.assertEquals("portal", versionService.getRepoName()); + } + + @Test + void testUpdateArtifactsInVersionWithProductArtifact() { + String version = "10.0.10"; + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifactModel artifactModel = new MavenArtifactModel(); + List mockMavenArtifactModels = List.of(artifactModel); + when(versionService.getProductJsonByVersion(Mockito.anyString())).thenReturn(List.of(new MavenArtifact())); + when(versionService.convertMavenArtifactsToModels(Mockito.anyList(), Mockito.anyString())) + .thenReturn(mockMavenArtifactModels); + Assertions.assertEquals(mockMavenArtifactModels, + versionService.updateArtifactsInVersionWithProductArtifact(version)); + Assertions.assertEquals(1, proceedDataCache.getVersions().size()); + Assertions.assertEquals(1, proceedDataCache.getProductArtifactWithVersionReleased().size()); + Assertions.assertEquals(version, proceedDataCache.getVersions().get(0)); + } + + @Test + void testSanitizeMetaArtifactBeforeHandle() { + setUpArtifactFromMeta(); + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String archivedArtifactId1 = "adobe-acrobat-sign-connector"; + String archivedArtifactId2 = "adobe-acrobat-sign-connector"; + ArchivedArtifact archivedArtifact1 = new ArchivedArtifact("10.0.10", groupId, archivedArtifactId1); + ArchivedArtifact archivedArtifact2 = new ArchivedArtifact("10.0.20", groupId, archivedArtifactId2); + artifactsFromMeta.get(1).setArchivedArtifacts(List.of(archivedArtifact2, archivedArtifact1)); + + versionService.sanitizeMetaArtifactBeforeHandle(); + String artifactId = "adobe-acrobat-sign-connector"; + + Assertions.assertEquals(1, artifactsFromMeta.size()); + Assertions.assertEquals(1, archivedArtifactsMap.size()); + Assertions.assertEquals(2, archivedArtifactsMap.get(artifactId).size()); + Assertions.assertEquals(archivedArtifact1, archivedArtifactsMap.get(artifactId).get(0)); + } + + @Test + void testGetVersionsToDisplay() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + versionFromArtifact.add("10.0.3-SNAPSHOT"); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(true, null)); + Assertions.assertEquals(List.of("10.0.5"), versionService.getVersionsToDisplay(null, "10.0.5")); + versionFromArtifact.remove("10.0.3-SNAPSHOT"); + Assertions.assertEquals(versionFromArtifact, versionService.getVersionsToDisplay(null, null)); + } + + @Test + void getVersionsFromMavenArtifacts() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String archivedArtifactId = "adobe-sign-connector"; + artifactsFromMeta.add(new MavenArtifact(repoUrl, null, groupId, artifactId, null, null, null, null)); + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.6"); + versionFromArtifact.add("10.0.5"); + versionFromArtifact.add("10.0.4"); + + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId)).thenReturn(versionFromArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + + List archivedArtifacts = List.of(new ArchivedArtifact("10.0.9", groupId, archivedArtifactId)); + ArrayList versionFromArchivedArtifact = new ArrayList<>(); + versionFromArchivedArtifact.add("10.0.3"); + versionFromArchivedArtifact.add("10.0.2"); + versionFromArchivedArtifact.add("10.0.1"); + artifactsFromMeta.get(0).setArchivedArtifacts(archivedArtifacts); + when(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, archivedArtifactId)) + .thenReturn(versionFromArchivedArtifact); + versionFromArtifact.addAll(versionFromArchivedArtifact); + Assertions.assertEquals(versionService.getVersionsFromMavenArtifacts(), versionFromArtifact); + } + + @Test + void testGetVersionsFromArtifactDetails() { + + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + + ArrayList versionFromArtifact = new ArrayList<>(); + versionFromArtifact.add("10.0.16"); + versionFromArtifact.add("10.0.18"); + versionFromArtifact.add("10.0.19"); + versionFromArtifact.add("10.0.20"); + versionFromArtifact.add("10.0.21"); + + try (MockedStatic xmlUtils = Mockito.mockStatic(XmlReaderUtils.class)) { + xmlUtils.when(() -> XmlReaderUtils.readXMLFromUrl(Mockito.anyString())).thenReturn(versionFromArtifact); + } + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, null, null), new ArrayList<>()); + Assertions.assertEquals(versionService.getVersionsFromArtifactDetails(repoUrl, groupId, artifactId), + versionFromArtifact); + } + + @Test + void testBuildMavenMetadataUrlFromArtifact() { + String repoUrl = "https://maven.axonivy.com"; + String groupId = "com.axonivy.connector.adobe.acrobat.sign"; + String artifactId = "adobe-acrobat-sign-connector"; + String metadataUrl = + "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/maven-metadata.xml"; + Assertions.assertEquals(StringUtils.EMPTY, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, null, artifactId)); + Assertions.assertEquals(StringUtils.EMPTY, versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, null), + StringUtils.EMPTY); + Assertions.assertEquals(metadataUrl, + versionService.buildMavenMetadataUrlFromArtifact(repoUrl, groupId, artifactId)); + } + + @Test + void testIsReleasedVersionOrUnReleaseDevVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + String unreleasedSprintVersion = "10.0.21-m1235"; + List versions = List.of(releasedVersion, snapshotVersion, sprintVersion, unreleasedSprintVersion); + Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, releasedVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, sprintVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, snapshotVersion)); + Assertions.assertFalse(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, minorSprintVersion)); + Assertions.assertTrue(versionService.isOfficialVersionOrUnReleasedDevVersion(versions, unreleasedSprintVersion)); + } + + @Test + void testGetBugfixVersion() { + String releasedVersion = "10.0.20"; + String snapshotVersion = "10.0.20-SNAPSHOT"; + String sprintVersion = "10.0.20-m1234"; + String minorSprintVersion = "10.0.20.1-m1234"; + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(releasedVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(snapshotVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(sprintVersion)); + Assertions.assertEquals(releasedVersion, versionService.getBugfixVersion(minorSprintVersion)); + } + + @Test + void testIsSnapshotVersion() { + String targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertTrue(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21-m1234"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSnapshotVersion(targetVersion)); + } + + @Test + void testIsSprintVersion() { + String targetVersion = "10.0.21-m1234"; + Assertions.assertTrue(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + + targetVersion = "10.0.21"; + Assertions.assertFalse(versionService.isSprintVersion(targetVersion)); + } + + @Test + void testIsReleasedVersion() { + String targetVersion = "10.0.21"; + Assertions.assertTrue(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + + targetVersion = "10.0.21-m1231"; + Assertions.assertFalse(versionService.isReleasedVersion(targetVersion)); + } + + @Test + void testIsMatchWithDesignerVersion() { + String designerVersion = "10.0.21"; + String targetVersion = "10.0.21.2"; + Assertions.assertTrue(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.21-SNAPSHOT"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + + targetVersion = "10.0.19"; + Assertions.assertFalse(versionService.isMatchWithDesignerVersion(targetVersion, designerVersion)); + } + + @Test + void testGetProductJsonByVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector.adobe.acrobat"; + GHContent mockContent = mock(GHContent.class); + repoName = "adobe-acrobat-sign-connector"; + ReflectionTestUtils.setField(versionService, "repoName", repoName); + ReflectionTestUtils.setField(versionService, "productId", "adobe-acrobat-connector"); + MavenArtifact productArtifact = + new MavenArtifact("https://maven.axonivy.com", null, targetGroupId, targetArtifactId, "iar", null, true, null); + + metaProductArtifact.setRepoUrl("https://maven.axonivy.com"); + metaProductArtifact.setGroupId(targetGroupId); + metaProductArtifact.setArtifactId(targetArtifactId); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(null); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + + metaProductArtifact.setGroupId("com.axonivy.connector.adobe.acrobat.connector"); + when(gitHubService.getContentFromGHRepoAndTag(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockContent); + + try { + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)).thenReturn(List.of(productArtifact)); + Assertions.assertEquals(1, versionService.getProductJsonByVersion("10.0.20").size()); + + when(gitHubService.convertProductJsonToMavenProductInfo(mockContent)) + .thenThrow(new IOException("Mock IO Exception")); + Assertions.assertEquals(0, versionService.getProductJsonByVersion("10.0.20").size()); + } catch (IOException e) { + Fail.fail("Mock setup should not throw an exception"); + } + } + + @Test + void testConvertMavenArtifactToModel() { + String downloadUrl = + "https://maven.axonivy.com/com/axonivy/connector/adobe/acrobat/sign/adobe-acrobat-sign-connector/10.0.21/adobe-acrobat-sign-connector-10.0.21.iar"; + String artifactName = "Adobe Acrobat Sign Connector (iar)"; + + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + + // Assert case handle artifact without name + MavenArtifactModel result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + MavenArtifactModel expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedResult.getName(), result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + + // Assert case handle artifact with name + artifactName = "Adobe Connector"; + String expectedArtifactName = "Adobe Connector (iar)"; + targetArtifact.setName(artifactName); + result = versionService.convertMavenArtifactToModel(targetArtifact, "10.0.21"); + expectedResult = new MavenArtifactModel(artifactName, downloadUrl, null); + Assertions.assertEquals(expectedArtifactName, result.getName()); + Assertions.assertEquals(expectedResult.getDownloadUrl(), result.getDownloadUrl()); + } + + @Test + void testConvertMavenArtifactsToModels() { + // Assert case param is empty + List result = versionService.convertMavenArtifactsToModels(Collections.emptyList(), "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is null + result = versionService.convertMavenArtifactsToModels(null, "10.0.21"); + Assertions.assertEquals(Collections.emptyList(), result); + + // Assert case param is a list with existed element + MavenArtifact targetArtifact = new MavenArtifact(null, null, "com.axonivy.connector.adobe.acrobat.sign", + "adobe-acrobat-sign-connector", null, null, null, null); + result = versionService.convertMavenArtifactsToModels(List.of(targetArtifact), "10.0.21"); + Assertions.assertEquals(1, result.size()); + } + + @Test + void testBuildDownloadUrlFromArtifactAndVersion() { + // Set up artifact for testing + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetGroupId = "com.axonivy.connector"; + MavenArtifact targetArtifact = + new MavenArtifact(null, null, targetGroupId, targetArtifactId, "iar", null, null, null); + String targetVersion = "10.0.10"; + + // Assert case without archived artifact + String expectedResult = + String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, MavenConstants.DEFAULT_IVY_MAVEN_BASE_URL, + "com/axonivy/connector", targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + String result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact not match & use custom repo + ArchivedArtifact adobeArchivedArtifactVersion9 = + new ArchivedArtifact("10.0.9", "com.axonivy.adobe.connector", "adobe-connector"); + ArchivedArtifact adobeArchivedArtifactVersion8 = + new ArchivedArtifact("10.0.8", "com.axonivy.adobe.sign.connector", "adobe-sign-connector"); + archivedArtifactsMap.put(targetArtifactId, List.of(adobeArchivedArtifactVersion9, adobeArchivedArtifactVersion8)); + String customRepoUrl = "https://nexus.axonivy.com"; + targetArtifact.setRepoUrl(customRepoUrl); + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, targetVersion); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, "com/axonivy/connector", + targetArtifactId, targetVersion, targetArtifactId, targetVersion, "iar"); + Assertions.assertEquals(expectedResult, result); + + // Assert case with artifact got matching archived artifact & use custom file + // type + String customType = "zip"; + targetArtifact.setType(customType); + targetVersion = "10.0.9"; + result = versionService.buildDownloadUrlFromArtifactAndVersion(targetArtifact, "10.0.9"); + expectedResult = String.format(MavenConstants.ARTIFACT_DOWNLOAD_URL_FORMAT, customRepoUrl, + "com/axonivy/adobe/connector", "adobe-connector", targetVersion, "adobe-connector", targetVersion, customType); + Assertions.assertEquals(expectedResult, result); + } + + @Test + void testFindArchivedArtifactInfoBestMatchWithVersion() { + String targetArtifactId = "adobe-acrobat-sign-connector"; + String targetVersion = "10.0.10"; + ArchivedArtifact result = + versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertNull(result); + + // Assert case with target version higher than all of latest version from + // archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion8 = + new ArchivedArtifact("10.0.8", "com.axonivy.connector", "adobe-sign-connector"); + ArchivedArtifact adobeArchivedArtifactVersion9 = + new ArchivedArtifact("10.0.9", "com.axonivy.connector", "adobe-acrobat-sign-connector"); + List archivedArtifacts = new ArrayList<>(); + archivedArtifacts.add(adobeArchivedArtifactVersion8); + archivedArtifacts.add(adobeArchivedArtifactVersion9); + archivedArtifactsMap.put(targetArtifactId, archivedArtifacts); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertNull(result); + + // Assert case with target version less than all of latest version from archived + // artifact list + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, "10.0.7"); + Assertions.assertEquals(adobeArchivedArtifactVersion8, result); + + // Assert case with target version is in range of archived artifact list + ArchivedArtifact adobeArchivedArtifactVersion10 = + new ArchivedArtifact("10.0.10", "com.axonivy.connector", "adobe-sign-connector"); + + archivedArtifactsMap.get(targetArtifactId).add(adobeArchivedArtifactVersion10); + result = versionService.findArchivedArtifactInfoBestMatchWithVersion(targetArtifactId, targetVersion); + Assertions.assertEquals(adobeArchivedArtifactVersion10, result); + } + + @Test + void testGetRepoNameFromMarketRepo() { + String defaultRepositoryName = "market/adobe-acrobat-connector"; + String expectedRepoName = "adobe-acrobat-connector"; + String result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "market/utils/adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + + defaultRepositoryName = "adobe-acrobat-connector"; + result = versionService.getRepoNameFromMarketRepo(defaultRepositoryName); + Assertions.assertEquals(expectedRepoName, result); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java new file mode 100644 index 000000000..9ffc48274 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java @@ -0,0 +1,93 @@ +package com.axonivy.market.util; + +import com.axonivy.market.constants.NonStandardProductPackageConstants; +import com.axonivy.market.github.util.GitHubUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GitHubUtilsTest { + private static final String JIRA_CONNECTOR = "Jira Connector"; + + @Test + void testConvertArtifactIdToName() { + String defaultArtifactId = "adobe-acrobat-sign-connector"; + String result = GitHubUtils.convertArtifactIdToName(defaultArtifactId); + Assertions.assertEquals("Adobe Acrobat Sign Connector", result); + + result = GitHubUtils.convertArtifactIdToName(null); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = GitHubUtils.convertArtifactIdToName(StringUtils.EMPTY); + Assertions.assertEquals(StringUtils.EMPTY, result); + + result = GitHubUtils.convertArtifactIdToName(" "); + Assertions.assertEquals(StringUtils.EMPTY, result); + } + + @Test + void testBuildProductJsonFilePath() { + String result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.PORTAL); + Assertions.assertEquals("AxonIvyPortal/portal-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.CONNECTIVITY_FEATURE); + Assertions.assertEquals("connectivity/connectivity-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.ERROR_HANDLING); + Assertions.assertEquals("error-handling/error-handling-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.WORKFLOW_DEMO); + Assertions.assertEquals("workflow/workflow-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_365); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-connector", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_CALENDAR); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-calendar", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TEAMS); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-chat", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_MAIL); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-mail", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.MICROSOFT_TODO); + Assertions.assertEquals("msgraph-connector-product/products/msgraph-todo", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.HTML_DIALOG_DEMO); + Assertions.assertEquals("html-dialog/html-dialog-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.RULE_ENGINE_DEMOS); + Assertions.assertEquals("rule-engine/rule-engine-demos-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_CONNECTOR); + Assertions.assertEquals("openai-connector-product", result); + + result = GitHubUtils.getNonStandardProductFilePath(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + Assertions.assertEquals("openai-assistant-product", result); + } + + @Test + void testGetNonStandardImageFolder() { + String result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXCEL_IMPORTER); + Assertions.assertEquals("doc", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.EXPRESS_IMPORTER); + Assertions.assertEquals("img", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.DEEPL_CONNECTOR); + Assertions.assertEquals("img", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.GRAPHQL_DEMO); + Assertions.assertEquals("assets", result); + + result = GitHubUtils.getNonStandardImageFolder(NonStandardProductPackageConstants.OPENAI_ASSISTANT); + Assertions.assertEquals("docs", result); + + result = GitHubUtils.getNonStandardImageFolder(JIRA_CONNECTOR); + Assertions.assertEquals("images", result); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java similarity index 58% rename from marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java rename to marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java index 7245ce561..dc755b0ac 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/utils/XmlReaderUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/XmlReaderUtilsTest.java @@ -1,4 +1,4 @@ -package com.axonivy.market.utils; +package com.axonivy.market.util; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; @@ -12,10 +12,10 @@ @ExtendWith(MockitoExtension.class) class XmlReaderUtilsTest { - @Test - void testExtractVersions() { - List versions = Collections.emptyList(); - XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); - Assertions.assertTrue(versions.isEmpty()); - } -} \ No newline at end of file + @Test + void testExtractVersions() { + List versions = Collections.emptyList(); + XmlReaderUtils.extractVersions(StringUtils.EMPTY, versions); + Assertions.assertTrue(versions.isEmpty()); + } +} From bf05c58b65b94b857033a1087da4cc6bc2b310ec Mon Sep 17 00:00:00 2001 From: Khanh Nguyen <119989010+ndkhanh-axonivy@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:38:34 +0700 Subject: [PATCH 2/5] Feature/MARP 357 create detail pages for new market website rating (#28) --- .github/workflows/service-dev-build.yml | 2 +- marketplace-service/pom.xml | 10 + .../market/MarketplaceServiceApplication.java | 6 +- .../assembler/FeedbackModelAssembler.java | 57 ++++++ .../assembler/ProductModelAssembler.java | 11 +- .../config/MarketApiDocumentConfig.java | 10 +- .../config/MarketHeaderInterceptor.java | 8 +- .../axonivy/market/config/MongoConfig.java | 11 +- .../market/constants/EntityConstants.java | 1 + .../market/constants/GitHubConstants.java | 17 ++ .../constants/RequestMappingConstants.java | 1 + .../market/controller/AppController.java | 12 +- .../market/controller/FeedbackController.java | 107 ++++++++++ .../market/controller/OAuth2Controller.java | 48 +++++ .../market/controller/ProductController.java | 44 ++-- .../market/controller/UserController.java | 27 --- .../com/axonivy/market/entity/Feedback.java | 55 +++++ .../axonivy/market/entity/GitHubRepoMeta.java | 7 +- .../market/entity/MavenArtifactModel.java | 9 +- .../java/com/axonivy/market/entity/User.java | 47 ++++- .../com/axonivy/market/enums/ErrorCode.java | 6 +- .../com/axonivy/market/enums/FileStatus.java | 4 +- .../com/axonivy/market/enums/FileType.java | 4 +- .../com/axonivy/market/enums/SortOption.java | 4 +- .../com/axonivy/market/enums/TypeOption.java | 4 +- .../market/exceptions/ExceptionHandlers.java | 44 +++- .../model/InvalidParamException.java | 1 - .../model/MissingHeaderException.java | 3 + .../exceptions/model/NotFoundException.java | 4 +- .../model/Oauth2ExchangeCodeException.java | 19 ++ .../market/github/model/ArchivedArtifact.java | 1 - .../market/github/model/GitHubFile.java | 5 +- .../com/axonivy/market/github/model/Meta.java | 5 +- .../service/GHAxonIvyMarketRepoService.java | 7 +- .../market/github/service/GitHubService.java | 22 +- .../impl/GHAxonIvyMarketRepoServiceImpl.java | 27 +-- .../service/impl/GitHubServiceImpl.java | 89 ++++++++- .../axonivy/market/model/DisplayValue.java | 6 +- .../axonivy/market/model/FeedbackModel.java | 42 ++++ .../market/model/MultilingualismValue.java | 4 +- .../market/model/Oauth2AuthorizationCode.java | 12 ++ .../axonivy/market/model/ProductModel.java | 14 +- .../axonivy/market/model/ProductRating.java | 16 ++ .../market/repository/FeedbackRepository.java | 21 ++ .../repository/GitHubRepoMetaRepository.java | 3 +- .../market/repository/ProductRepository.java | 3 +- .../market/repository/UserRepository.java | 1 + .../market/schedulingtask/ScheduledTasks.java | 6 +- .../market/service/FeedbackService.java | 17 ++ .../axonivy/market/service/JwtService.java | 10 + .../market/service/ProductService.java | 3 +- .../axonivy/market/service/UserService.java | 7 +- .../service/impl/FeedbackServiceImpl.java | 102 ++++++++++ .../market/service/impl/JwtServiceImpl.java | 54 +++++ .../market/service/impl/UserServiceImpl.java | 18 +- .../src/main/resources/application.properties | 4 + .../market/controller/AppControllerTest.java | 6 +- .../controller/FeedbackControllerTest.java | 161 +++++++++++++++ .../controller/OAuth2ControllerTest.java | 66 ++++++ .../controller/ProductControllerTest.java | 39 ++-- .../market/controller/UserControllerTest.java | 27 --- .../market/handler/ExceptionHandlersTest.java | 17 +- .../service/FeedbackServiceImplTest.java | 189 ++++++++++++++++++ .../GHAxonIvyMarketRepoServiceImplTest.java | 31 ++- .../market/service/GitHubServiceImplTest.java | 35 +++- .../market/service/JwtServiceImplTest.java | 94 +++++++++ .../market/service/SchedulingTasksTest.java | 7 +- .../market/service/UserServiceImplTest.java | 27 ++- 68 files changed, 1490 insertions(+), 291 deletions(-) create mode 100644 marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java delete mode 100644 marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java create mode 100644 marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java delete mode 100644 marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java create mode 100644 marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java diff --git a/.github/workflows/service-dev-build.yml b/.github/workflows/service-dev-build.yml index e310a9378..3700e5322 100644 --- a/.github/workflows/service-dev-build.yml +++ b/.github/workflows/service-dev-build.yml @@ -39,4 +39,4 @@ jobs: - name: Restart Tomcat server run: | sudo systemctl stop tomcat - sudo systemctl start tomcat + sudo systemctl start tomcat \ No newline at end of file diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index 947e700d7..d6a0d9a67 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -64,6 +64,16 @@ github-api 1.321 + + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.3.1 + diff --git a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java index 06660037c..52cdb27d9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java +++ b/marketplace-service/src/main/java/com/axonivy/market/MarketplaceServiceApplication.java @@ -1,5 +1,7 @@ package com.axonivy.market; +import com.axonivy.market.service.ProductService; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.time.StopWatch; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -9,10 +11,6 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -import com.axonivy.market.service.ProductService; - -import lombok.extern.log4j.Log4j2; - @Log4j2 @EnableAsync @EnableScheduling diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java new file mode 100644 index 000000000..a981b099a --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/FeedbackModelAssembler.java @@ -0,0 +1,57 @@ +package com.axonivy.market.assembler; + +import com.axonivy.market.controller.FeedbackController; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.FeedbackModel; +import com.axonivy.market.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@Log4j2 +@Component +public class FeedbackModelAssembler extends RepresentationModelAssemblerSupport { + + private final UserService userService; + + public FeedbackModelAssembler(UserService userService) { + super(Feedback.class, FeedbackModel.class); + this.userService = userService; + } + + @Override + public FeedbackModel toModel(Feedback feedback) { + FeedbackModel resource = new FeedbackModel(); + resource.add(linkTo(methodOn(FeedbackController.class).findFeedback(feedback.getId())) + .withSelfRel()); + return createResource(resource, feedback); + } + + private FeedbackModel createResource(FeedbackModel model, Feedback feedback) { + User user; + try { + user = userService.findUser(feedback.getUserId()); + } + catch (NotFoundException e) { + log.warn(e.getMessage()); + user = new User(); + } + model.setId(feedback.getId()); + model.setUsername(StringUtils.isBlank(user.getName()) ? user.getUsername() : user.getName()); + model.setUserAvatarUrl(user.getAvatarUrl()); + model.setUserProvider(user.getProvider()); + model.setProductId(feedback.getProductId()); + model.setContent(feedback.getContent()); + model.setRating(feedback.getRating()); + model.setCreatedAt(feedback.getCreatedAt()); + model.setUpdatedAt(feedback.getUpdatedAt()); + return model; + } + +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java index 00b94a52f..a50d82d1a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductModelAssembler.java @@ -1,14 +1,13 @@ package com.axonivy.market.assembler; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; - -import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; -import org.springframework.stereotype.Component; - import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductModel; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Component public class ProductModelAssembler extends RepresentationModelAssemblerSupport { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java index 61b6a0773..805f30120 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketApiDocumentConfig.java @@ -1,15 +1,15 @@ package com.axonivy.market.config; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; -import static com.axonivy.market.constants.CommonConstants.*; +import static com.axonivy.market.constants.CommonConstants.REQUESTED_BY; @Configuration public class MarketApiDocumentConfig { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java index 963706069..83b281062 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MarketHeaderInterceptor.java @@ -1,15 +1,13 @@ package com.axonivy.market.config; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.exceptions.model.MissingHeaderException; - import io.swagger.v3.oas.models.PathItem.HttpMethod; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; @Component public class MarketHeaderInterceptor implements HandlerInterceptor { diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java index a6cd2bc05..7f558f1cc 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/MongoConfig.java @@ -1,10 +1,15 @@ package com.axonivy.market.config; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -12,13 +17,9 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; - @Configuration @EnableMongoRepositories(basePackages = "com.axonivy.market.repository") +@EnableMongoAuditing public class MongoConfig extends AbstractMongoClientConfiguration { @Value("${spring.data.mongodb.host}") diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java index 76c1c45ab..0d9752cb9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/EntityConstants.java @@ -9,4 +9,5 @@ public class EntityConstants { public static final String PRODUCT = "Product"; public static final String MAVEN_ARTIFACT_VERSION = "MavenArtifactVersion"; public static final String GH_REPO_META = "GitHubRepoMeta"; + public static final String FEEDBACK = "Feedback"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java index 33c84df88..39d35a77c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/GitHubConstants.java @@ -10,4 +10,21 @@ public class GitHubConstants { public static final String AXONIVY_MARKETPLACE_PATH = "market"; public static final String DEFAULT_BRANCH = "feature/MARP-463-Multilingualism-for-Website"; public static final String PRODUCT_JSON_FILE_PATH_FORMAT = "%s/product.json"; + public static final String GITHUB_PROVIDER_NAME = "GitHub"; + public static final String GITHUB_GET_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Json { + public static final String ACCESS_TOKEN = "access_token"; + public static final String TOKEN = "token"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String CODE = "code"; + public static final String ERROR = "error"; + public static final String ERROR_DESCRIPTION = "error"; + public static final String USER_ID = "id"; + public static final String USER_NAME = "name"; + public static final String USER_AVATAR_URL = "avatar_url"; + public static final String USER_LOGIN_NAME = "login"; + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java index f4687a442..5efdc47e6 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestMappingConstants.java @@ -11,5 +11,6 @@ public class RequestMappingConstants { public static final String USER_MAPPING = "/user"; public static final String PRODUCT = API + "/product"; public static final String PRODUCT_DETAILS = API + "/product-details"; + public static final String FEEDBACK = API + "/feedback"; public static final String SWAGGER_URL = "/swagger-ui/index.html"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java index 60523b72a..45f712d2d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/AppController.java @@ -1,8 +1,8 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.ROOT; -import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; - +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,10 +10,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; - -import lombok.extern.log4j.Log4j2; +import static com.axonivy.market.constants.RequestMappingConstants.ROOT; +import static com.axonivy.market.constants.RequestMappingConstants.SWAGGER_URL; @Log4j2 @RestController 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 new file mode 100644 index 000000000..3232f42be --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/FeedbackController.java @@ -0,0 +1,107 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +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 io.jsonwebtoken.Claims; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +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.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; + +import static com.axonivy.market.constants.RequestMappingConstants.FEEDBACK; + +@RestController +@RequestMapping(FEEDBACK) +public class FeedbackController { + + private final FeedbackService feedbackService; + private final JwtService jwtService; + private final FeedbackModelAssembler feedbackModelAssembler; + + private final 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/{productId}") + public ResponseEntity> findFeedbacks(@PathVariable String productId, Pageable pageable) { + Page results = feedbackService.findFeedbacks(productId, pageable); + if (results.isEmpty()) { + return generateEmptyPagedModel(); + } + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); + var pageResources = pagedResourcesAssembler.toModel(responseContent, feedbackModelAssembler); + return new ResponseEntity<>(pageResources, HttpStatus.OK); + } + + @GetMapping("/{id}") + public ResponseEntity findFeedback(@PathVariable("id") 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 String userId, + @RequestParam String productId) { + Feedback feedback = feedbackService.findFeedbackByUserIdAndProductId(userId, productId); + return ResponseEntity.ok(feedbackModelAssembler.toModel(feedback)); + } + + @PostMapping + public ResponseEntity createFeedback(@RequestBody @Valid Feedback feedback, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { + String token = null; + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + token = authorizationHeader.substring(7); // Remove "Bearer " prefix + } + + // Validate the token + if (token == null || !jwtService.validateToken(token)) { + return ResponseEntity.status(401).build(); // Unauthorized if token is missing or invalid + } + + Claims claims = jwtService.getClaimsFromToken(token); + feedback.setUserId(claims.getSubject()); + Feedback newFeedback = feedbackService.upsertFeedback(feedback); + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}") + .buildAndExpand(newFeedback.getId()) + .toUri(); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "Find rating information of product by id") + @GetMapping("/product/{productId}/rating") + public ResponseEntity> getProductRating(@PathVariable("productId") String productId) { + return ResponseEntity.ok(feedbackService.getProductRatingById(productId)); + } + + @SuppressWarnings("unchecked") + private ResponseEntity> generateEmptyPagedModel() { + var emptyPagedModel = (PagedModel) pagedResourcesAssembler + .toEmptyModel(Page.empty(), FeedbackModel.class); + return new ResponseEntity<>(emptyPagedModel, HttpStatus.OK); + } +} 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 new file mode 100644 index 000000000..a3ebbca64 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -0,0 +1,48 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Map; + +@RestController +@RequestMapping("/auth") +public class OAuth2Controller { + + @Value("${spring.security.oauth2.client.registration.github.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.github.client-secret}") + private String clientSecret; + + private final GitHubService gitHubService; + + private final JwtService jwtService; + + public OAuth2Controller(GitHubService gitHubService, JwtService jwtService) { + this.gitHubService = gitHubService; + this.jwtService = jwtService; + } + + @PostMapping("/github/login") + public ResponseEntity gitHubLogin(@RequestBody Oauth2AuthorizationCode oauth2AuthorizationCode) { + Map tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), clientId, clientSecret); + String accessToken = (String) tokenResponse.get(GitHubConstants.Json.ACCESS_TOKEN); + + User user = gitHubService.getAndUpdateUser(accessToken); + + String jwtToken = jwtService.generateToken(user); + + return ResponseEntity.ok().body(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken)); + } +} \ No newline at end of file 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 ce273cd33..6dbd73c4d 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,8 +1,12 @@ package com.axonivy.market.controller; -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; -import static com.axonivy.market.constants.RequestMappingConstants.SYNC; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.model.Message; +import com.axonivy.market.model.ProductModel; +import com.axonivy.market.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; import org.apache.commons.lang3.time.StopWatch; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -11,32 +15,22 @@ import org.springframework.hateoas.PagedModel; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.model.Message; -import com.axonivy.market.model.ProductModel; -import com.axonivy.market.service.ProductService; - -import io.swagger.v3.oas.annotations.Operation; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT; +import static com.axonivy.market.constants.RequestMappingConstants.SYNC; @RestController @RequestMapping(PRODUCT) public class ProductController { - private final ProductService service; + private final ProductService productService; private final ProductModelAssembler assembler; private final PagedResourcesAssembler pagedResourcesAssembler; - public ProductController(ProductService service, ProductModelAssembler assembler, - PagedResourcesAssembler pagedResourcesAssembler) { - this.service = service; + public ProductController(ProductService productService, ProductModelAssembler assembler, + PagedResourcesAssembler pagedResourcesAssembler) { + this.productService = productService; this.assembler = assembler; this.pagedResourcesAssembler = pagedResourcesAssembler; } @@ -44,14 +38,14 @@ public ProductController(ProductService service, ProductModelAssembler assembler @Operation(summary = "Find all products", description = "Be default system will finds product by type as 'all'") @GetMapping() public ResponseEntity> findProducts( - @RequestParam(required = true, name = "type") String type, + @RequestParam(name = "type") String type, @RequestParam(required = false, name = "keyword") String keyword, - @RequestParam(required = true, name = "language") String language, Pageable pageable) { - Page results = service.findProducts(type, keyword, language, pageable); + @RequestParam(name = "language") String language, Pageable pageable) { + Page results = productService.findProducts(type, keyword, language, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); } - var responseContent = new PageImpl(results.getContent(), pageable, results.getTotalElements()); + var responseContent = new PageImpl<>(results.getContent(), pageable, results.getTotalElements()); var pageResources = pagedResourcesAssembler.toModel(responseContent, assembler); return new ResponseEntity<>(pageResources, HttpStatus.OK); } @@ -60,7 +54,7 @@ public ResponseEntity> findProducts( public ResponseEntity syncProducts() { var stopWatch = new StopWatch(); stopWatch.start(); - var isAlreadyUpToDate = service.syncLatestDataFromMarketRepo(); + var isAlreadyUpToDate = productService.syncLatestDataFromMarketRepo(); var message = new Message(); message.setHelpCode(ErrorCode.SUCCESSFUL.getCode()); message.setHelpText(ErrorCode.SUCCESSFUL.getHelpText()); diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java deleted file mode 100644 index c83c7cc2b..000000000 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/UserController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import com.axonivy.market.entity.User; -import com.axonivy.market.service.UserService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static com.axonivy.market.constants.RequestMappingConstants.USER_MAPPING; - -@RestController -@RequestMapping(USER_MAPPING) -public class UserController { - private final UserService userService; - - public UserController(UserService userService) { - this.userService = userService; - } - - @GetMapping - public ResponseEntity> getAllUser() { - return ResponseEntity.ok(userService.getAllUsers()); - } -} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java new file mode 100644 index 000000000..166da0c23 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Feedback.java @@ -0,0 +1,55 @@ +package com.axonivy.market.entity; + +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.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +import static com.axonivy.market.constants.EntityConstants.FEEDBACK; + +@Getter +@Setter +@NoArgsConstructor +@Document(FEEDBACK) +public class Feedback implements Serializable { + + @Serial + private static final long serialVersionUID = 29519800556564714L; + + @Id + private String id; + + private String userId; + + @NotBlank(message = "Product id cannot be blank") + private String productId; + + @NotBlank(message = "Content cannot be blank") + @Size(max = 5, message = "Content length must be up to 250 characters") + 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") + private Integer rating; + + @CreatedDate + private Date createdAt; + + @LastModifiedDate + private Date updatedAt; + + public void setContent(String content) { + this.content = content != null ? content.trim() : null; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java index 2e0770816..d2ef46fbf 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/GitHubRepoMeta.java @@ -1,12 +1,11 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; - +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Getter; -import lombok.Setter; +import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; @Getter @Setter 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 6b5328e26..2d48d4c6a 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,14 +1,13 @@ package com.axonivy.market.entity; -import java.io.Serializable; -import java.util.Objects; - -import org.springframework.data.annotation.Transient; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.Objects; @AllArgsConstructor @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java index 1b88f1095..0f8e7b612 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/User.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/User.java @@ -1,21 +1,48 @@ package com.axonivy.market.entity; -import static com.axonivy.market.constants.EntityConstants.USER; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serial; +import java.io.Serializable; + +import static com.axonivy.market.constants.EntityConstants.USER; @Getter @Setter @NoArgsConstructor @Document(USER) -public class User { - @Id - private String id; - private String username; - private String password; +public class User implements Serializable { + @Serial + private static final long serialVersionUID = -1244486023332931059L; + + @Id + private String id; + + @Indexed(unique = true) + private String gitHubId; + + private String provider; + private String username; + private String name; + private String avatarUrl; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((User) obj).getId()).isEquals(); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java index de9d54c28..7aef2b47c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/ErrorCode.java @@ -18,8 +18,12 @@ public enum ErrorCode { SUCCESSFUL("0000", "SUCCESSFUL"), PRODUCT_FILTER_INVALID("1101", "PRODUCT_FILTER_INVALID"), PRODUCT_SORT_INVALID("1102", "PRODUCT_SORT_INVALID"), + PRODUCT_NOT_FOUND("1103", "PRODUCT_NOT_FOUND"), GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"), - GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"); + GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), + USER_NOT_FOUND("2103", "USER_NOT_FOUND"), + FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), + ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST"); String code; String helpText; diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java index d75ca9f54..eb155031f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileStatus.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java index 75bb5beb9..7703e3cbd 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/FileType.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.NotFoundException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java index 59914ab90..c3e9714e3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/SortOption.java @@ -1,11 +1,9 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter @AllArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java index 3b513ea4a..1c30aca92 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/TypeOption.java @@ -1,10 +1,8 @@ package com.axonivy.market.enums; -import org.apache.commons.lang3.StringUtils; - import com.axonivy.market.exceptions.model.InvalidParamException; - import lombok.Getter; +import org.apache.commons.lang3.StringUtils; @Getter public enum TypeOption { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java index 3dc315608..d9b1ab725 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/ExceptionHandlers.java @@ -1,19 +1,47 @@ package com.axonivy.market.exceptions; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.model.Message; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import java.util.ArrayList; +import java.util.List; @ControllerAdvice public class ExceptionHandlers extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + BindingResult bindingResult = ex.getBindingResult(); + List errors = new ArrayList<>(); + if (bindingResult.hasErrors()) { + for (FieldError fieldError : bindingResult.getFieldErrors()) { + errors.add(fieldError.getDefaultMessage()); + } + } else { + errors.add(ex.getMessage()); + } + + var errorMessage = new Message(); + errorMessage.setHelpCode(ErrorCode.ARGUMENT_BAD_REQUEST.getCode()); + errorMessage.setMessageDetails(ErrorCode.ARGUMENT_BAD_REQUEST.getHelpText() + " - " + String.join("; ", errors)); + return new ResponseEntity<>(errorMessage, status); + } + @ExceptionHandler(MissingHeaderException.class) public ResponseEntity handleMissingServletRequestParameter(MissingHeaderException missingHeaderException) { var errorMessage = new Message(); @@ -36,4 +64,12 @@ public ResponseEntity handleInvalidException(InvalidParamException inval errorMessage.setMessageDetails(invalidDataException.getMessage()); return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(Oauth2ExchangeCodeException.class) + public ResponseEntity handleOauth2ExchangeCodeException(Oauth2ExchangeCodeException oauth2ExchangeCodeException) { + var errorMessage = new Message(); + errorMessage.setHelpCode(oauth2ExchangeCodeException.getError()); + errorMessage.setMessageDetails(oauth2ExchangeCodeException.getErrorDescription()); + return new ResponseEntity<>(errorMessage, HttpStatus.BAD_REQUEST); + } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java index 8a82188fa..3cecdf03e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/InvalidParamException.java @@ -1,7 +1,6 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.Getter; import lombok.Setter; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java index 4b5b158c6..2b6978f7f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/MissingHeaderException.java @@ -1,9 +1,12 @@ package com.axonivy.market.exceptions.model; +import java.io.Serial; + import static com.axonivy.market.constants.ErrorMessageConstants.INVALID_MISSING_HEADER_ERROR_MESSAGE; public class MissingHeaderException extends Exception { + @Serial private static final long serialVersionUID = 1L; public MissingHeaderException() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java index e1c917749..e84dd9c01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/NotFoundException.java @@ -1,16 +1,18 @@ package com.axonivy.market.exceptions.model; import com.axonivy.market.enums.ErrorCode; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import java.io.Serial; + @Getter @Setter @AllArgsConstructor public class NotFoundException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; private static final String SEPARATOR = "-"; diff --git a/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java new file mode 100644 index 000000000..d48a88770 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/exceptions/model/Oauth2ExchangeCodeException.java @@ -0,0 +1,19 @@ +package com.axonivy.market.exceptions.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; + +@Getter +@Setter +@AllArgsConstructor +public class Oauth2ExchangeCodeException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6778659816121728814L; + + private String error; + private String errorDescription; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java index 1bde19a0e..f9ff5a69f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/ArchivedArtifact.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java index 9586f0886..ca5d5ba70 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/GitHubFile.java @@ -1,14 +1,13 @@ package com.axonivy.market.github.model; -import java.util.Date; - import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Date; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java index 92e940487..918b90378 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/model/Meta.java @@ -1,16 +1,15 @@ package com.axonivy.market.github.model; -import java.util.List; - import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; + @Getter @Setter @NoArgsConstructor diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java index a669fac2a..8a3cb88f3 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GHAxonIvyMarketRepoService.java @@ -1,13 +1,12 @@ package com.axonivy.market.github.service; -import java.util.List; -import java.util.Map; - +import com.axonivy.market.github.model.GitHubFile; import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHRepository; -import com.axonivy.market.github.model.GitHubFile; +import java.util.List; +import java.util.Map; public interface GHAxonIvyMarketRepoService { diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java index 7dd5009db..5cf1a9d01 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/GitHubService.java @@ -1,22 +1,28 @@ package com.axonivy.market.github.service; -import java.io.IOException; -import java.util.List; - +import com.axonivy.market.entity.User; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; +import java.io.IOException; +import java.util.List; +import java.util.Map; + public interface GitHubService { - public GitHub getGitHub() throws IOException; + GitHub getGitHub() throws IOException; + + GHOrganization getOrganization(String orgName) throws IOException; + + GHRepository getRepository(String repositoryPath) throws IOException; - public GHOrganization getOrganization(String orgName) throws IOException; + List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; - public GHRepository getRepository(String repositoryPath) throws IOException; + GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; - public List getDirectoryContent(GHRepository ghRepository, String path, String ref) throws IOException; + Map getAccessToken(String code, String clientId, String clientSecret); - public GHContent getGHContent(GHRepository ghRepository, String path, String ref) throws IOException; + User getAndUpdateUser(String accessToken); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java index 6c96bee26..61aca55a9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GHAxonIvyMarketRepoServiceImpl.java @@ -1,21 +1,5 @@ package com.axonivy.market.github.service.impl; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.kohsuke.github.GHCommit; -import org.kohsuke.github.GHCommitQueryBuilder; -import org.kohsuke.github.GHCompare; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.springframework.stereotype.Service; - import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; @@ -23,8 +7,17 @@ import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; import com.axonivy.market.github.service.GitHubService; import com.axonivy.market.github.util.GitHubUtils; - import lombok.extern.log4j.Log4j2; +import org.kohsuke.github.*; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Log4j2 @Service diff --git a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java index 62bbd9181..6af97d1fa 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/github/service/impl/GitHubServiceImpl.java @@ -1,26 +1,40 @@ package com.axonivy.market.github.service.impl; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.List; - -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.repository.UserRepository; +import org.kohsuke.github.*; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ResourceUtils; +import org.springframework.web.client.RestTemplate; -import com.axonivy.market.github.service.GitHubService; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.Map; @Service public class GitHubServiceImpl implements GitHubService { + private final RestTemplate restTemplate; + private final UserRepository userRepository; + private static final String GITHUB_TOKEN_FILE = "classpath:github.token"; + public GitHubServiceImpl(RestTemplateBuilder restTemplateBuilder, UserRepository userRepository) { + this.restTemplate = restTemplateBuilder.build(); + this.userRepository = userRepository; + } + @Override public GitHub getGitHub() throws IOException { File gitHubToken = ResourceUtils.getFile(GITHUB_TOKEN_FILE); @@ -49,4 +63,57 @@ public GHContent getGHContent(GHRepository ghRepository, String path, String ref Assert.notNull(ghRepository, "Repository must not be null"); return ghRepository.getFileContent(path, ref); } + + @Override + public Map getAccessToken(String code, String clientId, String clientSecret) throws Oauth2ExchangeCodeException { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(GitHubConstants.Json.CLIENT_ID, clientId); + params.add(GitHubConstants.Json.CLIENT_SECRET, clientSecret); + params.add(GitHubConstants.Json.CODE, code); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.postForEntity(GitHubConstants.GITHUB_GET_ACCESS_TOKEN_URL, request, Map.class); + if (response.getBody().containsKey(GitHubConstants.Json.ERROR)) { + throw new Oauth2ExchangeCodeException(response.getBody().get(GitHubConstants.Json.ERROR).toString(), response.getBody().get(GitHubConstants.Json.ERROR_DESCRIPTION).toString()); + } + return response.getBody(); + } + + @Override + public User getAndUpdateUser(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + "https://api.github.com/user", HttpMethod.GET, entity, Map.class); + + Map userDetails = response.getBody(); + + if (userDetails == null) { + throw new RuntimeException("Failed to fetch user details from GitHub"); + } + + String gitHubId = userDetails.get(GitHubConstants.Json.USER_ID).toString(); + String name = (String) userDetails.get(GitHubConstants.Json.USER_NAME); + String avatarUrl = (String) userDetails.get(GitHubConstants.Json.USER_AVATAR_URL); + String username = (String) userDetails.get(GitHubConstants.Json.USER_LOGIN_NAME); + + User user = userRepository.searchByGitHubId(gitHubId); + if (user == null) { + user = new User(); + } + user.setGitHubId(gitHubId); + user.setName(name); + user.setUsername(username); + user.setAvatarUrl(avatarUrl); + user.setProvider(GitHubConstants.GITHUB_PROVIDER_NAME); + + userRepository.save(user); + + return user; + } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java index cd0bf4abf..70d54b588 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/DisplayValue.java @@ -1,15 +1,13 @@ package com.axonivy.market.model; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; @Getter @Setter 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 new file mode 100644 index 000000000..5eb1769ce --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/FeedbackModel.java @@ -0,0 +1,42 @@ +package com.axonivy.market.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.Date; + +@Getter +@Setter +@NoArgsConstructor +@Relation(collectionRelation = "feedbacks", itemRelation = "feedback") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FeedbackModel extends RepresentationModel { + private String id; + private String username; + private String userAvatarUrl; + private String userProvider; + private String productId; + private String content; + private Integer rating; + private Date createdAt; + private Date updatedAt; + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return new EqualsBuilder().append(id, ((FeedbackModel) obj).getId()).isEquals(); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java index 58431cf8e..389c4832e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/MultilingualismValue.java @@ -1,11 +1,11 @@ package com.axonivy.market.model; -import java.io.Serializable; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; + @Getter @Setter @NoArgsConstructor 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 new file mode 100644 index 000000000..56706c4c1 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/Oauth2AuthorizationCode.java @@ -0,0 +1,12 @@ +package com.axonivy.market.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Oauth2AuthorizationCode { + public String code; +} 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 7ba586438..0984f8765 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 @@ -1,18 +1,16 @@ package com.axonivy.market.model; -import java.util.List; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.core.Relation; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.core.Relation; + +import java.util.List; @Getter @Setter 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 new file mode 100644 index 000000000..b151e05f4 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductRating.java @@ -0,0 +1,16 @@ +package com.axonivy.market.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ProductRating { + private Integer starRating; + private Integer commentNumber; + private Integer percent; +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java new file mode 100644 index 000000000..bbb1fc301 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/FeedbackRepository.java @@ -0,0 +1,21 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Feedback; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FeedbackRepository extends MongoRepository { + + @Query("{ 'productId': ?0 }") + Page searchByProductId(String productId, Pageable pageable); + + List findByProductId(String productId); + + Feedback findByUserIdAndProductId(String userId, String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java index 49424d63c..6dda95549 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/GitHubRepoMetaRepository.java @@ -1,8 +1,7 @@ package com.axonivy.market.repository; -import org.springframework.data.mongodb.repository.MongoRepository; - import com.axonivy.market.entity.GitHubRepoMeta; +import org.springframework.data.mongodb.repository.MongoRepository; public interface GitHubRepoMetaRepository extends MongoRepository { diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java index 7fabd79bc..b638e30ea 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductRepository.java @@ -1,13 +1,12 @@ package com.axonivy.market.repository; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; -import com.axonivy.market.entity.Product; - import java.util.Optional; @Repository diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java index 6a011e9c6..30969ab97 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/UserRepository.java @@ -6,4 +6,5 @@ @Repository public interface UserRepository extends MongoRepository { + User searchByGitHubId(String gitHubId); } diff --git a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java index 96153ba39..5621c2d84 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java +++ b/marketplace-service/src/main/java/com/axonivy/market/schedulingtask/ScheduledTasks.java @@ -1,11 +1,9 @@ package com.axonivy.market.schedulingtask; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - import com.axonivy.market.service.ProductService; - import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; @Log4j2 @Component 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 new file mode 100644 index 000000000..1e8988f22 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/FeedbackService.java @@ -0,0 +1,17 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface FeedbackService { + Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException; + Feedback findFeedback(String id) throws NotFoundException; + Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException; + Feedback upsertFeedback(Feedback feedback) throws NotFoundException; + List getProductRatingById(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java new file mode 100644 index 000000000..49f1c9d44 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/JwtService.java @@ -0,0 +1,10 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import io.jsonwebtoken.Claims; + +public interface JwtService { + String generateToken(User user); + boolean validateToken(String token); + Claims getClaimsFromToken(String token); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java index b90604d42..a44f60668 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ProductService.java @@ -1,10 +1,9 @@ package com.axonivy.market.service; +import com.axonivy.market.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import com.axonivy.market.entity.Product; - public interface ProductService { Page findProducts(String type, String keyword, String language, Pageable pageable); diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java index 99c08f031..b6c064b4f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/UserService.java @@ -1,9 +1,12 @@ package com.axonivy.market.service; -import java.util.List; - import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; + +import java.util.List; public interface UserService { List getAllUsers(); + User createUser(User user); + User findUser(String id) throws NotFoundException; } 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 new file mode 100644 index 000000000..74ae9a998 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/FeedbackServiceImpl.java @@ -0,0 +1,102 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.UserService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class FeedbackServiceImpl implements FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final UserRepository userRepository; + private final ProductRepository productRepository; + + public FeedbackServiceImpl(FeedbackRepository feedbackRepository, UserRepository userRepository, ProductRepository productRepository, UserService userService) { + this.feedbackRepository = feedbackRepository; + this.userRepository = userRepository; + this.productRepository = productRepository; + } + + @Override + public Page findFeedbacks(String productId, Pageable pageable) throws NotFoundException { + validateProductExists(productId); + return feedbackRepository.searchByProductId(productId, pageable); + } + + @Override + public Feedback findFeedback(String id) throws NotFoundException { + return feedbackRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, "Not found feedback with id: " + id)); + } + + @Override + public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException { + userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + userId)); + validateProductExists(productId); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId); + if (existingUserFeedback == null) { + throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND, String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId)); + } + return existingUserFeedback; + } + + @Override + public Feedback upsertFeedback(Feedback feedback) throws NotFoundException { + userRepository.findById(feedback.getUserId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND,"Not found user with id: " + feedback.getUserId())); + + Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(feedback.getUserId(), feedback.getProductId()); + if (existingUserFeedback == null) { + return feedbackRepository.save(feedback); + } else { + existingUserFeedback.setRating(feedback.getRating()); + existingUserFeedback.setContent(feedback.getContent()); + return feedbackRepository.save(existingUserFeedback); + } + } + + @Override + public List getProductRatingById(String productId) { + List feedbacks = feedbackRepository.findByProductId(productId); + int totalFeedbacks = feedbacks.size(); + + if (totalFeedbacks == 0) { + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> new ProductRating(star, 0, 0)) + .collect(Collectors.toList()); + } + + Map ratingCountMap = feedbacks.stream() + .collect(Collectors.groupingBy(Feedback::getRating, Collectors.counting())); + + return IntStream.rangeClosed(1, 5) + .mapToObj(star -> { + long count = ratingCountMap.getOrDefault(star, 0L); + int percent = (int) ((count * 100) / totalFeedbacks); + return new ProductRating(star, Math.toIntExact(count), percent); + }) + .collect(Collectors.toList()); + } + + private void validateProductExists(String productId) throws NotFoundException { + productRepository.findById(productId) + .orElseThrow(() -> new NotFoundException(ErrorCode.PRODUCT_NOT_FOUND, "Not found product with id: " + productId)); + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java new file mode 100644 index 000000000..979bec72c --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/JwtServiceImpl.java @@ -0,0 +1,54 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.JwtService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class JwtServiceImpl implements JwtService { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(User user) { + Map claims = new HashMap<>(); + claims.put("name", user.getName()); + claims.put("username", user.getUsername()); + return Jwts.builder() + .setClaims(claims) + .setSubject(user.getId()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration * 86400000)) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + public boolean validateToken(String token) { + try { + getClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public Claims getClaimsFromToken(String token) { + return getClaimsJws(token).getBody(); + } + + public Jws getClaimsJws(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java index dc9990949..750bf3995 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/UserServiceImpl.java @@ -1,12 +1,13 @@ package com.axonivy.market.service.impl; -import java.util.List; - -import org.springframework.stereotype.Service; - import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.exceptions.model.NotFoundException; import com.axonivy.market.repository.UserRepository; import com.axonivy.market.service.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; @Service public class UserServiceImpl implements UserService { @@ -22,4 +23,13 @@ public List getAllUsers() { return userRepository.findAll(); } + @Override + public User findUser(String id) throws NotFoundException { + return userRepository.findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND, "Not found user with id: " + id)); + } + + @Override + public User createUser(User user) { + return userRepository.save(user); + } } diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 458102046..ea43ce9a9 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,3 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +spring.security.oauth2.client.registration.github.client-id=Ov23liUzb36JCQIfEBGn +spring.security.oauth2.client.registration.github.client-secret=d57a58cdc87bc9301d45fde3e2bdf3bff22fcbe1 +jwt.secret=dc4de2d13eaa5be9c185a8814c3afeac36440b19e0955aa7a5eecb7aa27b4fa4 +jwt.expiration=365 diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java index 104006d9d..055b81d2c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/AppControllerTest.java @@ -1,14 +1,14 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.springframework.http.HttpStatus; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + @ExtendWith(SpringExtension.class) class AppControllerTest { 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 new file mode 100644 index 000000000..55817625b --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/FeedbackControllerTest.java @@ -0,0 +1,161 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.assembler.FeedbackModelAssembler; +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.service.FeedbackService; +import com.axonivy.market.service.JwtService; +import com.axonivy.market.service.UserService; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.PagedModel; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FeedbackControllerTest { + + private static final String PRODUCT_ID_SAMPLE = "product-id"; + private static final String FEEDBACK_ID_SAMPLE = "feedback-id"; + private static final String USER_ID_SAMPLE = "user-id"; + private static final String USER_NAME_SAMPLE = "Test User"; + private static final String TOKEN_SAMPLE = "token-sample"; + + @Mock + private FeedbackService service; + + @Mock + private JwtService jwtService; + + @Mock + private UserService userService; + + @Mock + private FeedbackModelAssembler feedbackModelAssembler; + + @Mock + private PagedResourcesAssembler pagedResourcesAssembler; + + @InjectMocks + private FeedbackController feedbackController; + + @BeforeEach + void setup() { + feedbackModelAssembler = new FeedbackModelAssembler(userService); + feedbackController = new FeedbackController(service, jwtService, feedbackModelAssembler, pagedResourcesAssembler); + } + + @Test + void testFindFeedbacksAsEmpty() { + PageRequest pageable = PageRequest.of(0, 20); + Page mockFeedbacks = new PageImpl<>(List.of(), pageable, 0); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(pagedResourcesAssembler.toEmptyModel(any(), any())).thenReturn(PagedModel.empty()); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(0, result.getBody().getContent().size()); + } + + @Test + void testFindFeedbacks() { + PageRequest pageable = PageRequest.of(0, 20); + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + + Page mockFeedbacks = new PageImpl<>(List.of(mockFeedback), pageable, 1); + when(service.findFeedbacks(any(), any())).thenReturn(mockFeedbacks); + when(userService.findUser(any())).thenReturn(mockUser); + var mockFeedbackModel = feedbackModelAssembler.toModel(mockFeedback); + var mockPagedModel = PagedModel.of(List.of(mockFeedbackModel), new PagedModel.PageMetadata(1, 0, 1)); + when(pagedResourcesAssembler.toModel(any(), any(FeedbackModelAssembler.class))).thenReturn(mockPagedModel); + var result = feedbackController.findFeedbacks(PRODUCT_ID_SAMPLE, pageable); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(1, result.getBody().getContent().size()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getContent().iterator().next().getUsername()); + } + + @Test + void testFindFeedback() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedback(FEEDBACK_ID_SAMPLE)).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedback(FEEDBACK_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testFindFeedbackByUserIdAndProductId() { + Feedback mockFeedback = createFeedbackMock(); + User mockUser = createUserMock(); + when(service.findFeedbackByUserIdAndProductId(any(), any())).thenReturn(mockFeedback); + when(userService.findUser(any())).thenReturn(mockUser); + var result = feedbackController.findFeedbackByUserIdAndProductId(USER_ID_SAMPLE, PRODUCT_ID_SAMPLE); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertTrue(result.hasBody()); + assertEquals(USER_NAME_SAMPLE, result.getBody().getUsername()); + } + + @Test + void testCreateFeedback() { + 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); + + var result = feedbackController.createFeedback(mockFeedback, "Bearer " + TOKEN_SAMPLE); + assertEquals(HttpStatus.CREATED, result.getStatusCode()); + assertTrue(result.getHeaders().getLocation().toString().contains(mockFeedback.getId())); + } + + private Feedback createFeedbackMock() { + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(FEEDBACK_ID_SAMPLE); + mockFeedback.setUserId(USER_ID_SAMPLE); + mockFeedback.setProductId(PRODUCT_ID_SAMPLE); + mockFeedback.setContent("Great product!"); + mockFeedback.setRating(5); + return mockFeedback; + } + + private User createUserMock() { + User mockUser = new User(); + mockUser.setId(USER_ID_SAMPLE); + mockUser.setUsername("testUser"); + mockUser.setName("Test User"); + mockUser.setAvatarUrl("http://avatar.url"); + mockUser.setProvider("local"); + return mockUser; + } + + private Claims createMockClaims() { + Claims claims = new io.jsonwebtoken.impl.DefaultClaims(); + claims.setSubject(USER_ID_SAMPLE); + return claims; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java new file mode 100644 index 000000000..eff891be0 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -0,0 +1,66 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.entity.User; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.model.Oauth2AuthorizationCode; +import com.axonivy.market.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OAuth2ControllerTest { + + @Mock + private GitHubService gitHubService; + + @Mock + private JwtService jwtService; + + @InjectMocks + private OAuth2Controller oAuth2Controller; + + private Oauth2AuthorizationCode oauth2AuthorizationCode; + + @BeforeEach + void setup() { + oauth2AuthorizationCode = new Oauth2AuthorizationCode(); + oauth2AuthorizationCode.setCode("sampleCode"); + } + + @Test + void testGitHubLogin() { + String accessToken = "sampleAccessToken"; + User user = createUserMock(); + String jwtToken = "sampleJwtToken"; + + when(gitHubService.getAccessToken(any(), any(), any())).thenReturn(Map.of("access_token", accessToken)); + when(gitHubService.getAndUpdateUser(accessToken)).thenReturn(user); + when(jwtService.generateToken(user)).thenReturn(jwtToken); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(200, response.getStatusCodeValue()); + assertEquals(Map.of("token", jwtToken), response.getBody()); + } + + private User createUserMock() { + User user = new User(); + user.setId("userId"); + user.setUsername("username"); + user.setName("User Name"); + user.setAvatarUrl("http://avatar.url"); + user.setProvider("github"); + return user; + } +} \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java index 00417f662..0d984281e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductControllerTest.java @@ -1,12 +1,13 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.List; - +import com.axonivy.market.assembler.ProductModelAssembler; +import com.axonivy.market.entity.Product; +import com.axonivy.market.enums.ErrorCode; +import com.axonivy.market.enums.SortOption; +import com.axonivy.market.enums.TypeOption; +import com.axonivy.market.model.MultilingualismValue; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,16 +24,16 @@ import org.springframework.hateoas.PagedModel.PageMetadata; import org.springframework.http.HttpStatus; -import com.axonivy.market.assembler.ProductModelAssembler; -import com.axonivy.market.entity.Product; -import com.axonivy.market.enums.ErrorCode; -import com.axonivy.market.enums.SortOption; -import com.axonivy.market.enums.TypeOption; -import com.axonivy.market.model.MultilingualismValue; -import com.axonivy.market.service.ProductService; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ProductControllerTest { + private static final String PRODUCT_ID_SAMPLE = "amazon-comprehend"; private static final String PRODUCT_NAME_SAMPLE = "Amazon Comprehend"; private static final String PRODUCT_NAME_DE_SAMPLE = "Amazon Comprehend DE"; private static final String PRODUCT_DESC_SAMPLE = "Amazon Comprehend is a AI service that uses machine learning to uncover information in unstructured data."; @@ -112,4 +113,12 @@ private Product createProductMock() { mockProduct.setTags(List.of("AI")); return mockProduct; } -} \ No newline at end of file + + private ProductRating createProductRatingMock() { + ProductRating productRatingMock = new ProductRating(); + productRatingMock.setStarRating(1); + productRatingMock.setPercent(10); + productRatingMock.setCommentNumber(5); + return productRatingMock; + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java deleted file mode 100644 index 5886b6473..000000000 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/UserControllerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.axonivy.market.controller; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.axonivy.market.service.UserService; - -@ExtendWith(MockitoExtension.class) -class UserControllerTest { - - @Mock - UserService userService; - - @InjectMocks - UserController userController; - - @Test - void testGetAllUser() { - var result = userController.getAllUser(); - assertNotEquals(null, result); - } -} diff --git a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java index 3bbe6f80b..ad5db1617 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/handler/ExceptionHandlersTest.java @@ -1,9 +1,10 @@ package com.axonivy.market.handler; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import com.axonivy.market.exceptions.ExceptionHandlers; +import com.axonivy.market.exceptions.model.InvalidParamException; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,11 +12,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import com.axonivy.market.exceptions.ExceptionHandlers; -import com.axonivy.market.exceptions.model.InvalidParamException; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.NotFoundException; -import com.axonivy.market.model.Message; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ExceptionHandlersTest { 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 new file mode 100644 index 000000000..51a510357 --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/FeedbackServiceImplTest.java @@ -0,0 +1,189 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.Feedback; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.model.ProductRating; +import com.axonivy.market.repository.FeedbackRepository; +import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.FeedbackServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FeedbackServiceImplTest { + + @Mock + private FeedbackRepository feedbackRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private FeedbackServiceImpl feedbackService; + + @BeforeEach + void setUp() { + // Mock initialization or setup if needed + } + + @Test + void testFindFeedbacks_ProductNotFound() { + String productId = "nonExistingProduct"; + + when(productRepository.findById(productId)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbacks(productId, Pageable.unpaged())); + + verify(productRepository, times(1)).findById(productId); + verify(feedbackRepository, never()).searchByProductId(any(), any()); + } + + @Test + void testFindFeedback_Success() throws NotFoundException { + // Mock data + String feedbackId = "feedback123"; + Feedback mockFeedback = new Feedback(); + mockFeedback.setId(feedbackId); + + // Mock behavior + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(mockFeedback)); + + // Test method + Feedback result = feedbackService.findFeedback(feedbackId); + + // Verify + assertEquals(mockFeedback, result); + verify(feedbackRepository, times(1)).findById(feedbackId); + } + + @Test + void testFindFeedback_NotFound() { + // Mock data + String nonExistingId = "nonExistingFeedbackId"; + + // Mock behavior + when(feedbackRepository.findById(nonExistingId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedback(nonExistingId)); + + // Verify interactions + verify(feedbackRepository, times(1)).findById(nonExistingId); + } + + @Test + void testFindFeedbackByUserIdAndProductId_UserNotFound() { + // Mock data + String nonExistingUserId = "nonExistingUser"; + String productId = "product123"; + + // Mock behavior + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Test and verify exception + assertThrows(NotFoundException.class, () -> feedbackService.findFeedbackByUserIdAndProductId(nonExistingUserId, productId)); + + // Verify interactions + verify(userRepository, times(1)).findById(nonExistingUserId); + verify(feedbackRepository, never()).findByUserIdAndProductId(any(), any()); + } + + @Test + void testUpsertFeedback_NewFeedback() throws NotFoundException { + // Mock data + Feedback newFeedback = new Feedback(); + newFeedback.setUserId("user123"); + newFeedback.setProductId("product123"); + newFeedback.setContent("Great product!"); + newFeedback.setRating(5); + + User u = new User(); + u.setId(newFeedback.getUserId()); + when(userRepository.findById(newFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId())).thenReturn(null); + when(feedbackRepository.save(newFeedback)).thenReturn(newFeedback); + + // Test method + Feedback result = feedbackService.upsertFeedback(newFeedback); + + // Verify + assertEquals(newFeedback, result); + verify(userRepository, times(1)).findById(newFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(newFeedback.getUserId(), newFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(newFeedback); + } + + @Test + void testUpsertFeedback_UpdateFeedback() throws NotFoundException { + // Mock data + Feedback existingFeedback = new Feedback(); + existingFeedback.setId("existingFeedback123"); + existingFeedback.setUserId("user123"); + existingFeedback.setProductId("product123"); + existingFeedback.setContent("Good product!"); + existingFeedback.setRating(4); + + User u = new User(); + u.setId(existingFeedback.getUserId()); + when(userRepository.findById(existingFeedback.getUserId())).thenReturn(Optional.of(u)); + when(feedbackRepository.findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId())).thenReturn(existingFeedback); + when(feedbackRepository.save(existingFeedback)).thenReturn(existingFeedback); + + // Test method + Feedback updatedFeedback = new Feedback(); + updatedFeedback.setId(existingFeedback.getId()); + updatedFeedback.setUserId(existingFeedback.getUserId()); + updatedFeedback.setProductId(existingFeedback.getProductId()); + updatedFeedback.setContent("Excellent product!"); + updatedFeedback.setRating(5); + + Feedback result = feedbackService.upsertFeedback(updatedFeedback); + + // Verify + assertEquals(updatedFeedback.getId(), result.getId()); + assertEquals(updatedFeedback.getContent(), result.getContent()); + assertEquals(updatedFeedback.getRating(), result.getRating()); + verify(userRepository, times(1)).findById(existingFeedback.getUserId()); + verify(feedbackRepository, times(1)).findByUserIdAndProductId(existingFeedback.getUserId(), existingFeedback.getProductId()); + verify(feedbackRepository, times(1)).save(existingFeedback); + } + + @Test + void testGetProductRatingById_NoFeedbacks() { + // Mock data + String productId = "product123"; + + // Mock behavior + when(feedbackRepository.findByProductId(productId)).thenReturn(new ArrayList<>()); + + // Test method + List result = feedbackService.getProductRatingById(productId); + + // Verify + assertEquals(5, result.size()); // Expect ratings for stars 1 to 5 + result.forEach(rating -> { + assertEquals(0, rating.getCommentNumber()); + assertEquals(0, rating.getPercent()); + }); + verify(feedbackRepository, times(1)).findByProductId(productId); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java index fa74bc54a..0e1f06f4b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GHAxonIvyMarketRepoServiceImplTest.java @@ -1,32 +1,27 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommit.File; -import org.kohsuke.github.GHCompare; +import org.kohsuke.github.*; import org.kohsuke.github.GHCompare.Commit; -import org.kohsuke.github.GHContent; -import org.kohsuke.github.GHOrganization; -import org.kohsuke.github.GHRepository; -import org.kohsuke.github.PagedIterable; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyMarketRepoServiceImpl; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class GHAxonIvyMarketRepoServiceImplTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java index e26226c6b..bbd8416fa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/GitHubServiceImplTest.java @@ -1,13 +1,8 @@ package com.axonivy.market.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; - import com.axonivy.market.github.service.impl.GitHubServiceImpl; +import com.axonivy.market.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHContent; @@ -15,7 +10,16 @@ import org.kohsuke.github.GitHub; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class GitHubServiceImplTest { @@ -27,9 +31,25 @@ class GitHubServiceImplTest { @Mock GHRepository ghRepository; + @Mock + private RestTemplateBuilder restTemplateBuilder; + + @Mock + private RestTemplate restTemplate; + + @Mock + private UserRepository userRepository; + @InjectMocks private GitHubServiceImpl gitHubService; + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + // Use lenient stubbing + lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); + } + @Test void testGetGithub() throws IOException { var result = gitHubService.getGitHub(); @@ -51,5 +71,4 @@ void testGetDirectoryContent() throws IOException { var result = gitHubService.getDirectoryContent(ghRepository, "", ""); assertEquals(0, result.size()); } - } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java new file mode 100644 index 000000000..4eb8ffcec --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/JwtServiceImplTest.java @@ -0,0 +1,94 @@ +package com.axonivy.market.service; + +import com.axonivy.market.entity.User; +import com.axonivy.market.service.impl.JwtServiceImpl; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class JwtServiceImplTest { + + private static final String SECRET = "mySecret"; + private static final long EXPIRATION = 7L; // 7 days + + @InjectMocks + private JwtServiceImpl jwtService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(jwtService, "secret", SECRET); + ReflectionTestUtils.setField(jwtService, "expiration", EXPIRATION); + } + + @Test + void testGenerateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + assertNotNull(token); + assertFalse(token.isEmpty()); + + Claims claims = jwtService.getClaimsFromToken(token); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testValidateToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String validToken = jwtService.generateToken(user); + assertTrue(jwtService.validateToken(validToken)); + + String invalidToken = "invalid.token.here"; + assertFalse(jwtService.validateToken(invalidToken)); + } + + @Test + void testGetClaimsFromToken() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Claims claims = jwtService.getClaimsFromToken(token); + assertNotNull(claims); + assertEquals("123", claims.getSubject()); + assertEquals("John Doe", claims.get("name")); + assertEquals("johndoe", claims.get("username")); + } + + @Test + void testGetClaimsJws() { + User user = new User(); + user.setId("123"); + user.setName("John Doe"); + user.setUsername("johndoe"); + + String token = jwtService.generateToken(user); + + Jws claimsJws = jwtService.getClaimsJws(token); + assertNotNull(claimsJws); + assertNotNull(claimsJws.getBody()); + assertEquals("123", claimsJws.getBody().getSubject()); + } +} + diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java index f23d6ea21..8562d8f66 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/SchedulingTasksTest.java @@ -1,15 +1,14 @@ package com.axonivy.market.service; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.verify; - +import com.axonivy.market.schedulingtask.ScheduledTasks; import org.awaitility.Awaitility; import org.awaitility.Durations; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; -import com.axonivy.market.schedulingtask.ScheduledTasks; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; @SpringBootTest class SchedulingTasksTest { diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java index d14aaff8c..dd786a093 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/UserServiceImplTest.java @@ -1,7 +1,8 @@ package com.axonivy.market.service; -import java.util.List; - +import com.axonivy.market.entity.User; +import com.axonivy.market.repository.UserRepository; +import com.axonivy.market.service.impl.UserServiceImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,9 +11,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import com.axonivy.market.entity.User; -import com.axonivy.market.repository.UserRepository; -import com.axonivy.market.service.impl.UserServiceImpl; +import java.util.List; @ExtendWith(MockitoExtension.class) class UserServiceImplTest { @@ -28,8 +27,7 @@ void testFindAllUser() { // Mock data and service User mockUser = new User(); mockUser.setId("123"); - mockUser.setUsername("tvtTest"); - mockUser.setPassword("12345"); + mockUser.setName("tvtTest"); List mockResultReturn = List.of(mockUser); Mockito.when(userRepository.findAll()).thenReturn(mockResultReturn); @@ -39,4 +37,19 @@ void testFindAllUser() { // Verify Assertions.assertEquals(result, mockResultReturn); } + + @Test + void testCreateUser() { + // Mock data + User mockUser = new User(); + mockUser.setId("123"); + mockUser.setName("tvtTest"); + Mockito.when(userRepository.save(mockUser)).thenReturn(mockUser); + + // Exercise + User result = employeeService.createUser(mockUser); + + // Verify + Assertions.assertEquals(result, mockUser); + } } From 8ac647b64e3eccc15d119bf8ddd8b88ba0817f30 Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Fri, 12 Jul 2024 13:43:30 +0700 Subject: [PATCH 3/5] Update github oauth app client --- marketplace-service/src/main/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 458102046..34dc2446b 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,3 +9,7 @@ springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.patterns=http://localhost:[*], http://10.193.8.78:[*], http://marketplace.server.ivy-cloud.com:[*] market.cors.allowed.origin.maxAge=3600 +spring.security.oauth2.client.registration.github.client-id=Ov23liVMliBxBqdQ7FnG +spring.security.oauth2.client.registration.github.client-secret=97ee39cd07698bb95ead8b76ba25f2686a6cc7a6 +jwt.secret=dc4de2d13eaa5be9c185a8814c3afeac36440b19e0955aa7a5eecb7aa27b4fa4 +jwt.expiration=365 From f5797b4b43049f391fd94e3850a9589d7a0cb3fe Mon Sep 17 00:00:00 2001 From: Khanh Nguyen Date: Fri, 12 Jul 2024 14:26:06 +0700 Subject: [PATCH 4/5] Update FeedbackController.java --- .../java/com/axonivy/market/controller/FeedbackController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3232f42be..233c94b4e 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 @@ -43,7 +43,7 @@ public FeedbackController(FeedbackService feedbackService, JwtService jwtService @Operation(summary = "Find all feedbacks by product id") @GetMapping("/product/{productId}") - public ResponseEntity> findFeedbacks(@PathVariable String productId, Pageable pageable) { + public ResponseEntity> findFeedbacks(@PathVariable("productId") String productId, Pageable pageable) { Page results = feedbackService.findFeedbacks(productId, pageable); if (results.isEmpty()) { return generateEmptyPagedModel(); From 5608003a624c1455908e6655190baed1717f8cb3 Mon Sep 17 00:00:00 2001 From: Thuy Nguyen <145430420+nntthuy-axonivy@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:19:38 +0700 Subject: [PATCH 5/5] Feature/marp 223 detail pages for new market main content (#31) Fix async readme and product.json contents --- .../service/impl/ProductServiceImpl.java | 23 +++------------- .../ProductDetailsControllerTest.java | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 23 deletions(-) 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 80bfe0645..fd22c680c 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 @@ -6,10 +6,6 @@ import java.io.IOException; import java.net.URL; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.github.service.GHAxonIvyProductRepoService; @@ -264,23 +260,12 @@ private void updateProductFromReleaseTags(Product product) { product.setCompatibility(compatibility); } - List> completableFutures = new ArrayList<>(); - ExecutorService service = Executors.newFixedThreadPool(10); + List productModuleContents = new ArrayList<>(); for (GHTag ghtag : tags) { - completableFutures.add(CompletableFuture.supplyAsync( - () -> axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()), - service)); + ProductModuleContent productModuleContent = + axonIvyProductRepoService.getReadmeAndProductContentsFromTag(product, productRepo, ghtag.getName()); + productModuleContents.add(productModuleContent); } - completableFutures.forEach(CompletableFuture::join); - List productModuleContents = completableFutures.stream().map(completableFuture -> { - try { - return completableFuture.get(); - } catch (InterruptedException | ExecutionException e) { - Thread.currentThread().interrupt(); - log.error("Get readme and product json contents failed", e); - return null; - } - }).toList(); product.setProductModuleContents(productModuleContents); } catch (Exception e) { log.error("Cannot find repository by path {} {}", product.getRepositoryName(), e); diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java index 814044478..f188b3f4c 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ProductDetailsControllerTest.java @@ -28,6 +28,7 @@ @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { + public static final String TAG = "v10.0.6"; @Mock private ProductService productService; @@ -41,6 +42,7 @@ class ProductDetailsControllerTest { private ProductDetailsController productDetailsController; private static final String PRODUCT_NAME_SAMPLE = "Docker"; private static final String PRODUCT_NAME_DE_SAMPLE = "Docker DE"; + public static final String DOCKER_CONNECTOR_ID = "docker-connector"; @Test void testProductDetails() { @@ -49,15 +51,31 @@ void testProductDetails() { ResponseEntity mockExpectedResult = new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); - ResponseEntity result = productDetailsController.findProductDetails("docker-connector"); + ResponseEntity result = productDetailsController.findProductDetails(DOCKER_CONNECTOR_ID); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(result, mockExpectedResult); - verify(productService, times(1)).fetchProductDetail("docker-connector"); + verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); verify(detailModelAssembler, times(1)).toModel(mockProduct(), null); } + @Test + void testProductDetailsWithVersion() { + Mockito.when(productService.fetchProductDetail(Mockito.anyString())).thenReturn(mockProduct()); + Mockito.when(detailModelAssembler.toModel(mockProduct(), TAG)).thenReturn(createProductMockWithDetails()); + ResponseEntity mockExpectedResult = + new ResponseEntity<>(createProductMockWithDetails(), HttpStatus.OK); + + ResponseEntity result = + productDetailsController.findProductDetailsByVersion(DOCKER_CONNECTOR_ID, TAG); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(result, mockExpectedResult); + + verify(productService, times(1)).fetchProductDetail(DOCKER_CONNECTOR_ID); + } + @Test void testFindProductVersionsById() { List models = List.of(new MavenArtifactVersionModel()); @@ -73,7 +91,7 @@ void testFindProductVersionsById() { private Product mockProduct() { Product mockProduct = new Product(); - mockProduct.setId("docker-connector"); + mockProduct.setId(DOCKER_CONNECTOR_ID); MultilingualismValue name = new MultilingualismValue(); name.setEn(PRODUCT_NAME_SAMPLE); name.setDe(PRODUCT_NAME_DE_SAMPLE); @@ -84,7 +102,7 @@ private Product mockProduct() { private ProductDetailModel createProductMockWithDetails() { ProductDetailModel mockProductDetail = new ProductDetailModel(); - mockProductDetail.setId("docker-connector"); + mockProductDetail.setId(DOCKER_CONNECTOR_ID); MultilingualismValue name = new MultilingualismValue(); name.setEn(PRODUCT_NAME_SAMPLE); name.setDe(PRODUCT_NAME_DE_SAMPLE);