From 40e9d293c354516cd6fbcd27d1734016a8bef412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20V=C4=A9nh=20Thi=E1=BB=87n=20Ph=C3=BAc?= <143604440+tvtphuc-axonivy@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:04:27 +0700 Subject: [PATCH] Feature/marp 473 follow up for marp 264 installation frequency providing data (#35) --- .github/workflows/service-dev-build.yml | 2 +- .../ProductDetailModelAssembler.java | 1 + .../controller/ProductDetailsController.java | 24 ++++-- .../com/axonivy/market/entity/Product.java | 12 ++- .../market/model/ProductDetailModel.java | 1 + .../market/service/ProductService.java | 1 + .../service/impl/ProductServiceImpl.java | 73 ++++++++++++++-- .../src/main/resources/application.properties | 1 + .../ProductDetailsControllerTest.java | 23 +++-- .../service/ProductServiceImplTest.java | 86 ++++++++++++++++--- .../market/service/SchedulingTasksTest.java | 2 +- 11 files changed, 184 insertions(+), 42 deletions(-) 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/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java index dbfa0be0b..ff9ade568 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java +++ b/marketplace-service/src/main/java/com/axonivy/market/assembler/ProductDetailModelAssembler.java @@ -58,6 +58,7 @@ private void createDetailResource(ProductDetailModel model, Product product, Str model.setCompatibility(product.getCompatibility()); model.setContactUs(product.getContactUs()); model.setCost(product.getCost()); + model.setInstallationCount(product.getInstallationCount()); if (StringUtils.isBlank(tag) && StringUtils.isNotBlank(product.getNewestReleaseVersion())) { tag = product.getNewestReleaseVersion(); 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 e83712718..87fa6b218 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 @@ -1,22 +1,25 @@ package com.axonivy.market.controller; -import com.axonivy.market.model.MavenArtifactVersionModel; -import com.axonivy.market.service.VersionService; +import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; + +import java.util.List; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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 com.axonivy.market.assembler.ProductDetailModelAssembler; +import com.axonivy.market.model.MavenArtifactVersionModel; import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; -import org.springframework.web.bind.annotation.PathVariable; - -import java.util.List; - -import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DETAILS; +import io.swagger.v3.oas.annotations.Operation; @RestController @RequestMapping(PRODUCT_DETAILS) @@ -39,6 +42,13 @@ public ResponseEntity findProductDetailsByVersion(@PathVaria return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, tag), HttpStatus.OK); } + @Operation(summary = "increase installation count by 1", description = "increase installation count by 1") + @PutMapping("/installationcount/{key}") + public ResponseEntity syncInstallationCount(@PathVariable("key") String key) { + int result = productService.updateInstallationCountForProduct(key); + return new ResponseEntity<>(result, HttpStatus.OK); + } + @GetMapping("/{id}") public ResponseEntity findProductDetails(@PathVariable("id") String id) { var productDetail = productService.fetchProductDetail(id); 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 deb469a61..b1a135c31 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,15 +6,11 @@ import java.util.Date; import java.util.List; + 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 lombok.*; 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; @@ -24,6 +20,7 @@ @Setter @AllArgsConstructor @NoArgsConstructor +@Builder @Document(PRODUCT) public class Product implements Serializable { private static final long serialVersionUID = -8770801877877277258L; @@ -51,11 +48,12 @@ public class Product implements Serializable { private String compatibility; private Boolean validate; private Boolean contactUs; - private Integer installationCount; + private int installationCount; private Date newestPublishedDate; private String newestReleaseVersion; private List productModuleContents; private List artifacts; + private Boolean synchronizedInstallationCount; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java index 2943ccc79..99a13922f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java +++ b/marketplace-service/src/main/java/com/axonivy/market/model/ProductDetailModel.java @@ -22,6 +22,7 @@ public class ProductDetailModel extends ProductModel { private String compatibility; private Boolean contactUs; private ProductModuleContent productModuleContent; + private int installationCount; @Override public int hashCode() { 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 a44f60668..89bef2d95 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,6 +9,7 @@ public interface ProductService { boolean syncLatestDataFromMarketRepo(); + int updateInstallationCountForProduct(String key); 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 fd22c680c..04de791d5 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,18 +5,25 @@ import java.io.IOException; import java.net.URL; -import java.util.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; -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 com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.BooleanUtils; 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; import org.kohsuke.github.GHTag; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -26,19 +33,24 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; 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.GHAxonIvyProductRepoService; 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; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; @@ -54,9 +66,13 @@ public class ProductServiceImpl implements ProductService { private GHCommit lastGHCommit; private GitHubRepoMeta marketRepoMeta; + private final ObjectMapper mapper = new ObjectMapper(); - public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + @Value("${synchronized.installation.counts.path}") + private String installationCountPath; + public static final String NON_NUMERIC_CHAR = "[^0-9.]"; + private final Random random = new Random(); public ProductServiceImpl(ProductRepository productRepository, GHAxonIvyMarketRepoService axonIvyMarketRepoService, GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService) { @@ -108,6 +124,37 @@ public boolean syncLatestDataFromMarketRepo() { return isAlreadyUpToDate; } + @Override + public int updateInstallationCountForProduct(String key) { + return productRepository.findById(key).map(product -> { + log.info("updating installation count for product {}", key); + if (!BooleanUtils.isTrue(product.getSynchronizedInstallationCount())) { + syncInstallationCountWithProduct(product); + } + product.setInstallationCount(product.getInstallationCount() + 1); + return productRepository.save(product); + }).map(Product::getInstallationCount).orElse(0); + } + + private void syncInstallationCountWithProduct(Product product) { + log.info("synchronizing installation count for product {}", product.getId()); + try { + String installationCounts = Files.readString(Paths.get(installationCountPath)); + Map mapping = mapper.readValue(installationCounts, + new TypeReference>(){}); + List keyList = mapping.keySet().stream().toList(); + int currentInstallationCount = keyList.contains(product.getId()) + ? mapping.get(product.getId()) + : random.nextInt(20, 50); + product.setInstallationCount(currentInstallationCount); + product.setSynchronizedInstallationCount(true); + log.info("synchronized installation count for product {} successfully", product.getId()); + } catch (IOException ex) { + log.error(ex.getMessage()); + log.error("Could not read the marketplace-installation file to synchronize"); + } + } + private void syncRepoMetaDataStatus() { if (lastGHCommit == null) { return; @@ -273,6 +320,7 @@ private void updateProductFromReleaseTags(Product product) { } // Cover 3 cases after removing non-numeric characters (8, 11.1 and 10.0.2) + @Override public String getCompatibilityFromOldestTag(String oldestTag) { if (!oldestTag.contains(CommonConstants.DOT_SEPARATOR)) { return oldestTag + ".0+"; @@ -287,6 +335,13 @@ public String getCompatibilityFromOldestTag(String oldestTag) { @Override public Product fetchProductDetail(String id) { - return productRepository.findById(id).orElse(null); + Product product = productRepository.findById(id).orElse(null); + return Optional.ofNullable(product).map(productItem -> { + if (!BooleanUtils.isTrue(productItem.getSynchronizedInstallationCount())) { + syncInstallationCountWithProduct(productItem); + return productRepository.save(productItem); + } + return productItem; + }).orElse(null); } } diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 878e93e0f..aebb71018 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -9,6 +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 +synchronized.installation.counts.path=${MARKETPLACE_INSTALLATION_URL} spring.security.oauth2.client.registration.github.client-id= spring.security.oauth2.client.registration.github.client-secret= jwt.secret= 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 f188b3f4c..0ac125d07 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 @@ -3,28 +3,28 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Objects; -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; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -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.MavenArtifactVersionModel; +import com.axonivy.market.model.MultilingualismValue; import com.axonivy.market.model.ProductDetailModel; import com.axonivy.market.service.ProductService; +import com.axonivy.market.service.VersionService; @ExtendWith(MockitoExtension.class) class ProductDetailsControllerTest { @@ -89,6 +89,15 @@ void testFindProductVersionsById() { Assertions.assertEquals(models, result.getBody()); } + @Test + void testSyncInstallationCount() { + when(productService.updateInstallationCountForProduct("google-maps-connector")).thenReturn(1); + + var result = productDetailsController.syncInstallationCount("google-maps-connector"); + + assertEquals(1, result.getBody()); + } + private Product mockProduct() { Product mockProduct = new Product(); mockProduct.setId(DOCKER_CONNECTOR_ID); 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 9fd472991..841cfc7f5 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,49 +1,68 @@ package com.axonivy.market.service; import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.MetaConstants.META_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.META_FILE; 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.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; 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.*; +import org.kohsuke.github.GHCommit; +import org.kohsuke.github.GHContent; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; +import org.kohsuke.github.PagedIterable; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; 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.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; import com.axonivy.market.constants.GitHubConstants; import com.axonivy.market.entity.GitHubRepoMeta; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.enums.FileStatus; import com.axonivy.market.enums.FileType; import com.axonivy.market.enums.SortOption; import com.axonivy.market.enums.TypeOption; import com.axonivy.market.github.model.GitHubFile; import com.axonivy.market.github.service.GHAxonIvyMarketRepoService; +import com.axonivy.market.github.service.GHAxonIvyProductRepoService; 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; @@ -77,11 +96,13 @@ class ProductServiceImplTest { @Mock private GitHubService gitHubService; + @Captor + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Product.class); @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; @Captor - ArgumentCaptor> argumentCaptor; + ArgumentCaptor> productListArgumentCaptor; @InjectMocks private ProductServiceImpl productService; @@ -91,6 +112,50 @@ public void setup() { mockResultReturn = createPageProductsMock(); } + @Test + void testUpdateInstallationCount() { + // prepare + Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(mockProduct())); + + // exercise + productService.updateInstallationCountForProduct("google-maps-connector"); + + // Verify + verify(productRepository).save(argumentCaptor.capture()); + int updatedInstallationCount = argumentCaptor.getValue().getInstallationCount(); + + assertEquals(1, updatedInstallationCount); + verify(productRepository, times(1)).findById(Mockito.anyString()); + verify(productRepository, times(1)).save(Mockito.any()); + } + + @Test + void testSyncInstallationCountWithProduct() throws Exception { + // Mock data + ReflectionTestUtils.setField(productService, "installationCountPath", "path/to/installationCount.json"); + Product product = mockProduct(); + product.setSynchronizedInstallationCount(false); + Mockito.when(productRepository.findById("google-maps-connector")).thenReturn(Optional.of(product)); + Mockito.when(productRepository.save(any())).thenReturn(product); + // Mock the behavior of Files.readString and ObjectMapper.readValue + String installationCounts = "{\"google-maps-connector\": 10}"; + try (MockedStatic filesMockedStatic = mockStatic(Files.class)) { + when(Files.readString(Paths.get("path/to/installationCount.json"))).thenReturn(installationCounts); + // Call the method + int result = productService.updateInstallationCountForProduct("google-maps-connector"); + + // Verify the results + assertEquals(11, result); + assertEquals(true, product.getSynchronizedInstallationCount()); + assertTrue(product.getSynchronizedInstallationCount()); + } + } + + private Product mockProduct() { + return Product.builder().id("google-maps-connector").language("English").synchronizedInstallationCount(true) + .build(); + } + @Test void testFindProducts() { langague = "en"; @@ -240,9 +305,9 @@ void testSyncProductsFirstTime() throws IOException { // Executes productService.syncLatestDataFromMarketRepo(); - verify(productRepository).saveAll(argumentCaptor.capture()); + verify(productRepository).saveAll(productListArgumentCaptor.capture()); - assertThat(argumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() + assertThat(productListArgumentCaptor.getValue().get(0).getProductModuleContents()).usingRecursiveComparison() .isEqualTo(List.of(mockReadmeProductContent())); } @@ -277,6 +342,7 @@ void testSearchProducts() { void testFetchProductDetail() { String id = "amazon-comprehend"; Product mockProduct = mockResultReturn.getContent().get(0); + mockProduct.setSynchronizedInstallationCount(true); when(productRepository.findById(id)).thenReturn(Optional.ofNullable(mockProduct)); Product result = productService.fetchProductDetail(id); assertEquals(mockProduct, result); 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 8562d8f66..70e4d0771 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 @@ -10,7 +10,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; -@SpringBootTest +@SpringBootTest(properties = { "marketPlace-installation-url=D:/marketplace-installation.json" }) class SchedulingTasksTest { @SpyBean