diff --git a/.github/workflows/ui-ci-build.yml b/.github/workflows/ui-ci-build.yml index 0802e839d..157a8fd7f 100644 --- a/.github/workflows/ui-ci-build.yml +++ b/.github/workflows/ui-ci-build.yml @@ -39,8 +39,6 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - - name: Remove unused sonar images - run: docker image prune -af - name: Execute Tests run: | cd ./marketplace-ui @@ -64,3 +62,12 @@ jobs: env: SONAR_TOKEN: ${{ env.SONAR_TOKEN }} SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + + clean-up: + name: Remove unused docker images + needs: analysis + runs-on: self-hosted + + steps: + - name: Remove unused sonar images + run: docker image prune -af diff --git a/marketplace-build/.env b/marketplace-build/.env index 75cd6f6b7..ed05498de 100644 --- a/marketplace-build/.env +++ b/marketplace-build/.env @@ -9,4 +9,5 @@ MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= -MARKET_CORS_ALLOWED_ORIGIN=* \ No newline at end of file +MARKET_CORS_ALLOWED_ORIGIN=* +MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/marketplace-build/dev/.env b/marketplace-build/dev/.env index a476ec9f3..1a037765b 100644 --- a/marketplace-build/dev/.env +++ b/marketplace-build/dev/.env @@ -9,4 +9,5 @@ MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= -MARKET_CORS_ALLOWED_ORIGIN=* \ No newline at end of file +MARKET_CORS_ALLOWED_ORIGIN=* +MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/marketplace-build/dev/docker-compose.yml b/marketplace-build/dev/docker-compose.yml index 033964664..d7c8d3cd2 100644 --- a/marketplace-build/dev/docker-compose.yml +++ b/marketplace-build/dev/docker-compose.yml @@ -22,7 +22,7 @@ services: container_name: marketplace-service restart: always volumes: - - /home/axonivy/marketplace/data/market-installations.json:/home/data/market-installation.json + - /home/axonivy/marketplace/data/market-installations.json:/data/market-installation.json environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -34,6 +34,7 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} + - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} build: context: ../../marketplace-service dockerfile: Dockerfile diff --git a/marketplace-build/docker-compose.yml b/marketplace-build/docker-compose.yml index 230d269b6..d85991b79 100644 --- a/marketplace-build/docker-compose.yml +++ b/marketplace-build/docker-compose.yml @@ -37,7 +37,7 @@ services: container_name: marketplace-service restart: always volumes: - - /home/axonivy/marketplace/data/market-installations.json:/home/data/market-installation.json + - /home/axonivy/marketplace/data/market-installations.json:/data/market-installation.json environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -49,6 +49,7 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} + - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} build: context: ../marketplace-service dockerfile: Dockerfile diff --git a/marketplace-build/release/.env b/marketplace-build/release/.env index 2b5f160d4..9af06af38 100644 --- a/marketplace-build/release/.env +++ b/marketplace-build/release/.env @@ -10,4 +10,5 @@ MARKET_GITHUB_TOKEN= MARKET_GITHUB_OAUTH_APP_CLIENT_ID= MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET= MARKET_JWT_SECRET_KEY= -MARKET_CORS_ALLOWED_ORIGIN=* \ No newline at end of file +MARKET_CORS_ALLOWED_ORIGIN=* +MARKET_MONGO_LOG_LEVEL=DEBUG \ No newline at end of file diff --git a/marketplace-build/release/docker-compose.yml b/marketplace-build/release/docker-compose.yml index afb6b5825..06f69540e 100644 --- a/marketplace-build/release/docker-compose.yml +++ b/marketplace-build/release/docker-compose.yml @@ -29,7 +29,7 @@ services: expose: - 8080 volumes: - - /home/axonivy/marketplace/data/market-installations.json:/home/data/market-installation.json + - /home/axonivy/marketplace/data/market-installations.json:/data/market-installation.json environment: - MONGODB_HOST=${SERVICE_MONGODB_HOST} - MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE} @@ -41,5 +41,6 @@ services: - MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} - MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY} - MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN} + - MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL} volumes: mongodata: \ No newline at end of file diff --git a/marketplace-service/pom.xml b/marketplace-service/pom.xml index 1a19d1657..8bdb6e625 100644 --- a/marketplace-service/pom.xml +++ b/marketplace-service/pom.xml @@ -36,7 +36,6 @@ org.springframework.boot spring-boot-starter-tomcat - provided org.springframework.boot 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 8817c5929..9acbe6fc5 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 @@ -1,25 +1,24 @@ package com.axonivy.market.assembler; -import com.axonivy.market.constants.RequestMappingConstants; -import com.axonivy.market.controller.ProductDetailsController; -import com.axonivy.market.entity.Product; -import com.axonivy.market.model.ProductDetailModel; -import com.axonivy.market.util.VersionUtils; -import lombok.extern.log4j.Log4j2; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import java.util.Optional; + import org.apache.commons.lang3.StringUtils; import org.springframework.hateoas.Link; import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.util.CollectionUtils; -import java.util.Optional; - -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; +import com.axonivy.market.constants.RequestMappingConstants; +import com.axonivy.market.controller.ProductDetailsController; +import com.axonivy.market.entity.Product; +import com.axonivy.market.model.ProductDetailModel; +import com.axonivy.market.util.ImageUtils; +import com.axonivy.market.util.VersionUtils; @Component -@Log4j2 public class ProductDetailModelAssembler extends RepresentationModelAssemblerSupport { private final ProductModelAssembler productModelAssembler; @@ -80,6 +79,7 @@ private void createDetailResource(ProductDetailModel model, Product product) { model.setContactUs(product.getContactUs()); model.setCost(product.getCost()); model.setInstallationCount(product.getInstallationCount()); - model.setProductModuleContent(product.getProductModuleContent()); + model.setProductModuleContent(ImageUtils.mappingImageForProductModuleContent(product.getProductModuleContent())); } + } 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 a50d82d1a..9e51bb92a 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,8 +1,10 @@ package com.axonivy.market.assembler; +import com.axonivy.market.controller.ImageController; import com.axonivy.market.controller.ProductDetailsController; import com.axonivy.market.entity.Product; import com.axonivy.market.model.ProductModel; +import org.springframework.hateoas.Link; import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; import org.springframework.stereotype.Component; @@ -29,7 +31,9 @@ public ProductModel createResource(ProductModel model, Product product) { model.setShortDescriptions(product.getShortDescriptions()); model.setType(product.getType()); model.setTags(product.getTags()); - model.setLogoUrl(product.getLogoUrl()); + + Link logoLink = linkTo(methodOn(ImageController.class).findImageById(product.getLogoId())).withSelfRel(); + model.setLogoUrl(logoLink.getHref()); return model; } 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 0fb1dc852..67e0dfbed 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 @@ -9,6 +9,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Arrays; +import java.util.List; + import static com.axonivy.market.constants.CommonConstants.REQUESTED_BY; @Configuration @@ -20,16 +23,20 @@ public class MarketApiDocumentConfig { @Bean public GroupedOpenApi buildMarketCustomHeader() { - return GroupedOpenApi.builder().group(DEFAULT_DOC_GROUP).addOpenApiCustomizer(customMarketHeaders()) - .pathsToMatch(PATH_PATTERN).build(); + return GroupedOpenApi.builder().group(DEFAULT_DOC_GROUP) + .addOpenApiCustomizer(customMarketHeaders()) + .pathsToMatch(PATH_PATTERN).build(); } private OpenApiCustomizer customMarketHeaders() { return openApi -> openApi.getPaths().values().forEach((PathItem pathItem) -> { - for (Operation operation : pathItem.readOperations()) { - Parameter headerParameter = new Parameter().in(HEADER_PARAM).schema(new StringSchema()).name(REQUESTED_BY) - .description(DEFAULT_PARAM).required(true); - operation.addParametersItem(headerParameter); + List operations = Arrays.asList(pathItem.getPut(), pathItem.getPost(), pathItem.getPatch(), pathItem.getDelete()); + for (Operation operation : operations) { + if (operation != null) { + Parameter headerParameter = new Parameter().in(HEADER_PARAM).schema(new StringSchema()) + .name(REQUESTED_BY).description(DEFAULT_PARAM).required(true); + operation.addParametersItem(headerParameter); + } } }); } 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 83b281062..f47d860da 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 @@ -5,22 +5,21 @@ 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.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @Component public class MarketHeaderInterceptor implements HandlerInterceptor { - @Value("${request.header}") - private String requestHeader; - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) { return true; } - if (!requestHeader.equals(request.getHeader(CommonConstants.REQUESTED_BY))) { + if (!HttpMethod.GET.name().equalsIgnoreCase(request.getMethod()) + && StringUtils.isBlank(request.getHeader(CommonConstants.REQUESTED_BY))) { throw new MissingHeaderException(); } return true; diff --git a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java index 38d1e80f9..d50fe342c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java +++ b/marketplace-service/src/main/java/com/axonivy/market/config/WebConfig.java @@ -10,8 +10,8 @@ public class WebConfig implements WebMvcConfigurer { private static final String ALL_MAPPINGS = "/**"; - private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**", - "/api/product-details/productjsoncontent/**" , }; + private static final String[] EXCLUDE_PATHS = { "/", "/swagger-ui/**", "/api-docs/**", "/api/product-details/**/json", + "/api/image/**" }; private static final String[] ALLOWED_HEADERS = { "Accept-Language", "Content-Type", "Authorization", "X-Requested-By", "x-requested-with", "X-Forwarded-Host", "x-xsrf-token", "x-authorization" }; private static final String[] ALLOWED_METHODS = { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; 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 e21a1aa17..68a5f236a 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 @@ -6,7 +6,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class CommonConstants { public static final String REQUESTED_BY = "X-Requested-By"; - public static final String LOGO_FILE = "logo.png"; public static final String SLASH = "/"; public static final String DOT_SEPARATOR = "."; public static final String PLUS = "+"; @@ -14,4 +13,8 @@ public class CommonConstants { public static final String SPACE_SEPARATOR = " "; public static final String BEARER = "Bearer"; public static final String DIGIT_REGEX = "([0-9]+.*)"; + public static final String IMAGE_ID_PREFIX = "imageId-"; + public static final String ID_WITH_NUMBER_PATTERN = "%s-%s"; + public static final String ERROR = "error"; + public static final String MESSAGE = "message"; } 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 157aa49d4..2d0fee374 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 @@ -14,4 +14,5 @@ public class EntityConstants { public static final String PRODUCT_CUSTOM_SORT = "ProductCustomSort"; public static final String PRODUCT_JSON_CONTENT = "ProductJsonContent"; public static final String PRODUCT_MODULE_CONTENT = "ProductModuleContent"; + public static final String IMAGE = "Image"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java index 4ad9f831a..431d7311e 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/ErrorMessageConstants.java @@ -6,4 +6,5 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ErrorMessageConstants { public static final String INVALID_MISSING_HEADER_ERROR_MESSAGE = "Invalid or missing header"; + public static final String CURRENT_CLIENT_ID_MISMATCH_MESSAGE = " Client ID mismatch (Request ID: %s, Server ID: %s)"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java index 6a41ad547..917709dd9 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/MongoDBConstants.java @@ -6,16 +6,11 @@ private MongoDBConstants() { } public static final String ID ="_id"; - public static final String PRODUCT_MODULE_CONTENT ="productModuleContent"; - public static final String PRODUCT_MODULE_CONTENT_QUERY ="$productModuleContents"; - public static final String INPUT ="input"; - public static final String AS ="as"; - public static final String CONDITION ="cond"; - public static final String EQUAL ="$eq"; - public static final String PRODUCT_MODULE_CONTENT_TAG ="$$productModuleContent.tag"; public static final String PRODUCT_COLLECTION ="Product"; public static final String INSTALLATION_COUNT = "InstallationCount"; public static final String SYNCHRONIZED_INSTALLATION_COUNT ="SynchronizedInstallationCount"; public static final String PRODUCT_ID = "productId"; public static final String DESIGNER_VERSION = "designerVersion"; + public static final String TAG = "tag"; + } 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 c802ab60c..fd63733dd 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 @@ -2,6 +2,7 @@ public class ProductJsonConstants { public static final String PRODUCT_JSON_FILE = "product.json"; + public static final String LOGO_FILE = "logo.png"; public static final String DATA = "data"; public static final String REPOSITORIES = "repositories"; public static final String URL = "url"; 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 abfc30ba3..8d3124988 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 @@ -22,9 +22,10 @@ public class RequestMappingConstants { public static final String PRODUCT_BY_ID = "/product/{id}"; public static final String PRODUCT_RATING_BY_ID = "/product/{id}/rating"; public static final String INSTALLATION_COUNT_BY_ID = "/installationcount/{id}"; - public static final String PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION = "/productjsoncontent/{productId}/{version}"; + public static final String PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION = "/{id}/{version}/json"; public static final String VERSIONS_IN_DESIGNER = "/{id}/designerversions"; public static final String DESIGNER_INSTALLATION_BY_PRODUCT_ID_AND_DESIGNER_VERSION = "/installation/{productId}/designer/{designerVersion}"; - public static final String DESIGNER_INSTALLATION_BY_PRODUCT_ID = "/installation/{productId}/designer"; + public static final String DESIGNER_INSTALLATION_BY_ID = "/installation/{id}/designer"; public static final String CUSTOM_SORT = "custom-sort"; + public static final String IMAGE = API + "/image"; } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java index 5a5097edb..b1fff6169 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java +++ b/marketplace-service/src/main/java/com/axonivy/market/constants/RequestParamConstants.java @@ -6,7 +6,6 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class RequestParamConstants { public static final String ID = "id"; - public static final String TAG = "tag"; public static final String TYPE = "type"; public static final String KEYWORD = "keyword"; public static final String LANGUAGE = "language"; @@ -17,5 +16,4 @@ public class RequestParamConstants { public static final String SHOW_DEV_VERSION = "isShowDevVersion"; public static final String DESIGNER_VERSION = "designerVersion"; public static final String VERSION = "version"; - public static final String PRODUCT_ID = "productId"; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java new file mode 100644 index 000000000..5264d7e40 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ImageController.java @@ -0,0 +1,54 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.entity.Image; +import com.axonivy.market.service.ImageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.axonivy.market.constants.RequestMappingConstants.BY_ID; +import static com.axonivy.market.constants.RequestMappingConstants.IMAGE; +import static com.axonivy.market.constants.RequestParamConstants.ID; + +@RestController +@RequestMapping(IMAGE) +@Tag(name = "Image Controllers", description = "API collection to get image's detail.") +public class ImageController { + private final ImageService imageService; + + public ImageController(ImageService imageService) { + this.imageService = imageService; + } + + @GetMapping(BY_ID) + @Operation(summary = "Get the image content by id", description = "Collect the byte[] of image with contentType in header is PNG") + @ApiResponse(responseCode = "200", description = "Image found and returned", content = @Content(mediaType = MediaType.IMAGE_PNG_VALUE, schema = @Schema(implementation = Image.class))) + @ApiResponse(responseCode = "404", description = "Image not found") + @ApiResponse(responseCode = "204", description = "No content (image empty)") + public ResponseEntity findImageById( + @PathVariable(ID) @Parameter(description = "The image id", example = "66e7efc8a24f36158df06fc7", in = ParameterIn.PATH) String id) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_PNG); + byte[] imageData = imageService.readImage(id); + if (imageData == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + if (imageData.length == 0) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(imageData, headers, 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 index 928f2e203..afc4aba6d 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/OAuth2Controller.java @@ -4,8 +4,11 @@ import static com.axonivy.market.constants.RequestMappingConstants.GIT_HUB_LOGIN; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -54,12 +57,16 @@ public ResponseEntity> gitHubLogin(@RequestBody Oauth2Author try { GitHubAccessTokenResponse tokenResponse = gitHubService.getAccessToken(oauth2AuthorizationCode.getCode(), gitHubProperty); accessToken = tokenResponse.getAccessToken(); + User user = gitHubService.getAndUpdateUser(accessToken); + String jwtToken = jwtService.generateToken(user); + return new ResponseEntity<>(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken), HttpStatus.OK); + } catch (Oauth2ExchangeCodeException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(CommonConstants.ERROR, e.getError()); + errorResponse.put(CommonConstants.MESSAGE, e.getErrorDescription()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } catch (Exception e) { - return new ResponseEntity<>(Map.of(e.getClass().getName(), e.getMessage()), HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(Map.of(CommonConstants.MESSAGE, e.getMessage()), HttpStatus.BAD_REQUEST); } - - User user = gitHubService.getAndUpdateUser(accessToken); - String jwtToken = jwtService.generateToken(user); - return new ResponseEntity<>(Collections.singletonMap(GitHubConstants.Json.TOKEN, jwtToken), HttpStatus.OK); } } \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java index 645717df2..301cb7439 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java +++ b/marketplace-service/src/main/java/com/axonivy/market/controller/ProductDesignerInstallationController.java @@ -15,9 +15,9 @@ import java.util.List; -import static com.axonivy.market.constants.RequestMappingConstants.DESIGNER_INSTALLATION_BY_PRODUCT_ID; +import static com.axonivy.market.constants.RequestMappingConstants.DESIGNER_INSTALLATION_BY_ID; import static com.axonivy.market.constants.RequestMappingConstants.PRODUCT_DESIGNER_INSTALLATION; -import static com.axonivy.market.constants.RequestParamConstants.PRODUCT_ID; +import static com.axonivy.market.constants.RequestParamConstants.ID; @RestController @RequestMapping(PRODUCT_DESIGNER_INSTALLATION) @@ -29,9 +29,9 @@ public ProductDesignerInstallationController(ProductDesignerInstallationService this.productDesignerInstallationService = productDesignerInstallationService; } - @GetMapping(DESIGNER_INSTALLATION_BY_PRODUCT_ID) + @GetMapping(DESIGNER_INSTALLATION_BY_ID) @Operation(summary = "Get designer installation count by product id.", description = "get designer installation count by product id") - public ResponseEntity> getProductDesignerInstallationByProductId(@PathVariable(PRODUCT_ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String productId) { + public ResponseEntity> getProductDesignerInstallationByProductId(@PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "adobe-acrobat-connector", in = ParameterIn.PATH) String productId) { List models = productDesignerInstallationService.findByProductId(productId); return new ResponseEntity<>(models, 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 bdb4f4e84..5b68520bd 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 @@ -2,7 +2,6 @@ import static com.axonivy.market.constants.RequestParamConstants.DESIGNER_VERSION; import static com.axonivy.market.constants.RequestParamConstants.ID; -import static com.axonivy.market.constants.RequestParamConstants.PRODUCT_ID; import static com.axonivy.market.constants.RequestParamConstants.SHOW_DEV_VERSION; import static com.axonivy.market.constants.RequestParamConstants.VERSION; import static com.axonivy.market.constants.RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION; @@ -22,7 +21,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -47,7 +45,7 @@ public class ProductDetailsController { private final ProductDetailModelAssembler detailModelAssembler; public ProductDetailsController(VersionService versionService, ProductService productService, - ProductDetailModelAssembler detailModelAssembler) { + ProductDetailModelAssembler detailModelAssembler) { this.versionService = versionService; this.productService = productService; this.detailModelAssembler = detailModelAssembler; @@ -71,7 +69,6 @@ public ResponseEntity findBestMatchProductDetailsByVersion( return new ResponseEntity<>(detailModelAssembler.toModel(productDetail, version, BEST_MATCH_BY_ID_AND_VERSION), HttpStatus.OK); } - @CrossOrigin(originPatterns = "*") @PutMapping(INSTALLATION_COUNT_BY_ID) @Operation(summary = "Update installation count of product", description = "By default, increase installation count when click download product files by users") public ResponseEntity syncInstallationCount( @@ -101,7 +98,7 @@ public ResponseEntity> findProductVersionsById( @GetMapping(PRODUCT_JSON_CONTENT_BY_PRODUCT_ID_AND_VERSION) @Operation(summary = "Get product json content for designer to install", description = "When we click install in designer, this API will send content of product json for installing in Ivy designer") - public ResponseEntity> findProductJsonContent(@PathVariable(PRODUCT_ID) String productId, + public ResponseEntity> findProductJsonContent(@PathVariable(ID) String productId, @PathVariable(VERSION) String version) { Map productJsonContent = versionService.getProductJsonContentByIdAndVersion(productId, version); return new ResponseEntity<>(productJsonContent, HttpStatus.OK); @@ -113,5 +110,4 @@ public ResponseEntity> findVersionsForDesigner(@PathVar List versionList = versionService.getVersionsForDesigner(id); return new ResponseEntity<>(versionList, HttpStatus.OK); } - } 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 d2ef46fbf..2f07cdfbb 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 @@ -3,8 +3,11 @@ import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mongodb.core.mapping.Document; +import java.util.Date; + import static com.axonivy.market.constants.EntityConstants.GH_REPO_META; @Getter @@ -16,4 +19,6 @@ public class GitHubRepoMeta { private String repoName; private Long lastChange; private String lastSHA1; + @LastModifiedDate + private Date updatedAt; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java b/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java new file mode 100644 index 000000000..85e6842ef --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/Image.java @@ -0,0 +1,32 @@ +package com.axonivy.market.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.types.Binary; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import static com.axonivy.market.constants.EntityConstants.IMAGE; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Document(IMAGE) +public class Image { + @Id + private String id; + @Schema(description = "Product id", example = "jira-connector") + private String productId; + @Schema(description = "The download url from github", example = "https://raw.githubusercontent.comamazon-comprehend/logo.png") + private String imageUrl; + @Schema(description = "The image content as binary type", example = "Binary(Buffer.from(\"89504e470d0a1a0a0000000d\", \"hex\"), 0)") + private Binary imageData; + @Schema(description = "The SHA from github", example = "93b1e2f1595d3a85e51b01") + private String sha; +} 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 e11d7fba8..00db4d4e4 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 @@ -1,5 +1,6 @@ package com.axonivy.market.entity; +import static com.axonivy.market.constants.EntityConstants.PRODUCT; import com.axonivy.market.github.model.MavenArtifact; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -10,8 +11,8 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.Transient; -import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.io.Serial; @@ -20,8 +21,6 @@ import java.util.List; import java.util.Map; -import static com.axonivy.market.constants.EntityConstants.PRODUCT; - @Getter @Setter @AllArgsConstructor @@ -66,6 +65,9 @@ public class Product implements Serializable { private List releasedVersions; @Transient private String metaProductJsonUrl; + private String logoId; + @LastModifiedDate + private Date updatedAt; @Override public int hashCode() { diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java index 7c913c67d..127a92ead 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductDesignerInstallation.java @@ -8,7 +8,6 @@ import java.io.Serial; import java.io.Serializable; -import java.util.Date; import static com.axonivy.market.constants.EntityConstants.PRODUCT_DESIGNER_INSTALLATION; diff --git a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java index 0d8b2b79c..a6a48fb6f 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductJsonContent.java @@ -7,8 +7,11 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.mongodb.core.mapping.Document; +import java.util.Date; + import static com.axonivy.market.constants.EntityConstants.PRODUCT_JSON_CONTENT; @Getter @@ -25,4 +28,6 @@ public class ProductJsonContent { private String productId; private String name; private String content; -} + @LastModifiedDate + private Date updatedAt; +} \ 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 index 4001571d1..08762f4ed 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java +++ b/marketplace-service/src/main/java/com/axonivy/market/entity/ProductModuleContent.java @@ -7,10 +7,12 @@ import lombok.NoArgsConstructor; import lombok.Setter; 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 java.util.Map; import static com.axonivy.market.constants.EntityConstants.PRODUCT_MODULE_CONTENT; @@ -46,4 +48,6 @@ public class ProductModuleContent implements Serializable { private String artifactId; @Schema(description = "Artifact file type", example = "iar") private String type; + @LastModifiedDate + private Date updatedAt; } diff --git a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java index 0535ce237..4c914204a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java +++ b/marketplace-service/src/main/java/com/axonivy/market/enums/NonStandardProduct.java @@ -47,6 +47,6 @@ public enum NonStandardProduct { } public static NonStandardProduct findById(String id) { - return NON_STANDARD_PRODUCT_MAP.getOrDefault(id,DEFAULT); + return NON_STANDARD_PRODUCT_MAP.getOrDefault(id, DEFAULT); } } 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 e84dd9c01..11f9c2cf5 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,5 +1,6 @@ package com.axonivy.market.exceptions.model; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.enums.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,7 +15,6 @@ public class NotFoundException extends RuntimeException { @Serial private static final long serialVersionUID = 1L; - private static final String SEPARATOR = "-"; private final String code; private final String message; @@ -26,7 +26,7 @@ public NotFoundException(ErrorCode errorCode) { public NotFoundException(ErrorCode errorCode, String additionalMessage) { this.code = errorCode.getCode(); - this.message = errorCode.getHelpText() + SEPARATOR + additionalMessage; + this.message = errorCode.getHelpText() + CommonConstants.DASH_SEPARATOR + additionalMessage; } } 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 e6f5b7580..a1ee2fdce 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,8 +1,15 @@ package com.axonivy.market.factory; +import static com.axonivy.market.constants.CommonConstants.SLASH; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; +import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import com.axonivy.market.constants.CommonConstants; import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.github.model.Meta; -import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.DisplayValue; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; @@ -12,20 +19,12 @@ import org.apache.commons.lang3.StringUtils; import org.kohsuke.github.GHContent; import org.springframework.util.CollectionUtils; - import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; -import static com.axonivy.market.constants.CommonConstants.SLASH; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_NAME; -import static com.axonivy.market.constants.MetaConstants.DEFAULT_VENDOR_URL; -import static com.axonivy.market.constants.MetaConstants.META_FILE; -import static org.apache.commons.lang3.StringUtils.EMPTY; - @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ProductFactory { @@ -40,9 +39,6 @@ public static Product mappingByGHContent(Product product, GHContent content) { if (StringUtils.endsWith(contentName, META_FILE)) { mappingByMetaJSONFile(product, content); } - if (StringUtils.endsWith(contentName, LOGO_FILE)) { - product.setLogoUrl(GitHubUtils.getDownloadUrl(content)); - } return product; } @@ -78,6 +74,14 @@ public static Product mappingByMetaJSONFile(Product product, GHContent ghContent return product; } + public static void transferComputedPersistedDataToProduct(Product persisted, Product product) { + product.setCustomOrder(persisted.getCustomOrder()); + product.setNewestReleaseVersion(persisted.getNewestReleaseVersion()); + product.setReleasedVersions(persisted.getReleasedVersions()); + product.setInstallationCount(persisted.getInstallationCount()); + product.setSynchronizedInstallationCount(persisted.getSynchronizedInstallationCount()); + } + private static Map mappingMultilingualismValueByMetaJSONFile(List list) { Map value = new HashMap<>(); if (!CollectionUtils.isEmpty(list)) { @@ -112,4 +116,16 @@ public static void extractSourceUrl(Product product, Meta meta) { private static Meta jsonDecode(GHContent ghContent) throws IOException { return MAPPER.readValue(ghContent.read().readAllBytes(), Meta.class); } + + public static void mappingIdForProductModuleContent(ProductModuleContent content) { + if (StringUtils.isNotBlank(content.getProductId()) && StringUtils.isNotBlank(content.getTag())) { + content.setId(String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), content.getTag())); + } + } + + public static void mappingIdForProductJsonContent(ProductJsonContent content) { + if (StringUtils.isNotBlank(content.getProductId()) && StringUtils.isNotBlank(content.getVersion())) { + content.setId(String.format(CommonConstants.ID_WITH_NUMBER_PATTERN, content.getProductId(), content.getVersion())); + } + } } 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 5c857cde2..2d0e673f0 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 @@ -5,16 +5,19 @@ import com.axonivy.market.constants.MavenConstants; import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Image; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.entity.ProductJsonContent; import com.axonivy.market.enums.Language; import com.axonivy.market.enums.NonStandardProduct; +import com.axonivy.market.factory.ProductFactory; import com.axonivy.market.github.model.MavenArtifact; 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.ProductJsonContentRepository; +import com.axonivy.market.service.ImageService; import com.axonivy.market.util.VersionUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,6 +33,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import javax.swing.text.html.Option; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -38,9 +42,11 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; import static com.axonivy.market.constants.ProductJsonConstants.EN_LANGUAGE; import static com.axonivy.market.constants.ProductJsonConstants.VERSION_VALUE; @@ -49,10 +55,11 @@ public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoService { private GHOrganization organization; private final GitHubService gitHubService; - + private final ImageService imageService; private final ProductJsonContentRepository productJsonContentRepository; private String repoUrl; private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String HASH = "#"; 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[^)]*?)\\)"; @@ -61,9 +68,10 @@ public class GHAxonIvyProductRepoServiceImpl implements GHAxonIvyProductRepoServ public static final String DEMO = "demo"; public static final String SETUP = "setup"; - public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService, + public GHAxonIvyProductRepoServiceImpl(GitHubService gitHubService, ImageService imageService, ProductJsonContentRepository productJsonContentRepository) { this.gitHubService = gitHubService; + this.imageService = imageService; this.productJsonContentRepository = productJsonContentRepository; } @@ -167,6 +175,7 @@ public ProductModuleContent getReadmeAndProductContentsFromTag(Product product, List contents = getProductFolderContents(product, ghRepository, tag); productModuleContent.setProductId(product.getId()); productModuleContent.setTag(tag); + ProductFactory.mappingIdForProductModuleContent(productModuleContent); updateDependencyContentsFromProductJson(productModuleContent, contents , product); extractReadMeFileFromContents(product, contents, productModuleContent); } catch (Exception e) { @@ -237,13 +246,12 @@ private void updateDependencyContentsFromProductJson(ProductModuleContent produc productModuleContent.setName(artifact.getName()); } String currentVersion = VersionUtils.convertTagToVersion(productModuleContent.getTag()); - boolean isProductJsonContentExists = productJsonContentRepository.existsByProductIdAndVersion(product.getId(), - currentVersion); String content = extractProductJsonContent(productJsonFile, productModuleContent.getTag()); - if (ObjectUtils.isNotEmpty(content) && !isProductJsonContentExists) { + if (ObjectUtils.isNotEmpty(content)) { ProductJsonContent jsonContent = new ProductJsonContent(); jsonContent.setVersion(currentVersion); jsonContent.setProductId(product.getId()); + ProductFactory.mappingIdForProductJsonContent(jsonContent); jsonContent.setName(product.getNames().get(EN_LANGUAGE)); jsonContent.setContent(content.replace(VERSION_VALUE, currentVersion)); productJsonContentRepository.save(jsonContent); @@ -266,37 +274,42 @@ private static GHContent getProductJsonFile(List contents) { .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) + public String updateImagesWithDownloadUrl(Product product, List contents, String readmeContents) { + + List imagesAtRootFolder = 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); - } + + List allContentOfImages = ObjectUtils.isNotEmpty(imagesAtRootFolder) + ? imagesAtRootFolder + : getImagesFromImageFolder(product, contents); + + Map imageUrls = new HashMap<>(); + + allContentOfImages.forEach(content -> Optional.of(imageService.mappingImageFromGHContent(product, content, false)) + .ifPresent(image -> imageUrls.put(content.getName(), IMAGE_ID_PREFIX.concat(image.getId())))); + 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 { + private List getImagesFromImageFolder(Product product, List contents) { + List images = new ArrayList<>(); 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()); - } - } + contents.stream().filter(GHContent::isDirectory) + .filter(content -> imageFolderPath.equals(content.getName())) + .findFirst().ifPresent(imageFolder -> { + try { + images.addAll(imageFolder.listDirectoryContent().toList()); + } catch (IOException e) { + log.error(e.getMessage()); + } + }); + return images; } // Cover some cases including when demo and setup parts switch positions or @@ -361,10 +374,16 @@ private boolean hasImageDirectives(String readmeContents) { } private String removeFirstLine(String text) { + String result; if (text.isBlank()) { - return Strings.EMPTY; + result = Strings.EMPTY; + } else if (text.startsWith(HASH)) { + int index = text.indexOf(StringUtils.LF); + result = index != StringUtils.INDEX_NOT_FOUND ? text.substring(index + 1).trim() : Strings.EMPTY; + } else { + result = text; } - int index = text.indexOf(StringUtils.LF); - return index != -1 ? text.substring(index + 1).trim() : Strings.EMPTY; + + return result; } } 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 045d745f2..4ff0bc3e8 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,19 +1,26 @@ package com.axonivy.market.github.service.impl; -import static org.apache.commons.lang3.StringUtils.EMPTY; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ErrorMessageConstants; +import com.axonivy.market.constants.GitHubConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.enums.ErrorCode; +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.exceptions.model.UnauthorizedException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +import com.axonivy.market.github.model.GitHubProperty; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.repository.UserRepository; +import lombok.extern.log4j.Log4j2; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTag; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.GHTag; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; @@ -28,19 +35,15 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import com.axonivy.market.constants.GitHubConstants; -import com.axonivy.market.entity.User; -import com.axonivy.market.enums.ErrorCode; -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.exceptions.model.UnauthorizedException; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -import com.axonivy.market.github.model.GitHubProperty; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.util.GitHubUtils; -import com.axonivy.market.repository.UserRepository; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.apache.commons.lang3.StringUtils.EMPTY; +@Log4j2 @Service public class GitHubServiceImpl implements GitHubService { @@ -57,9 +60,8 @@ public GitHubServiceImpl(RestTemplateBuilder restTemplateBuilder, UserRepository @Override public GitHub getGitHub() throws IOException { - return new GitHubBuilder() - .withOAuthToken(Optional.ofNullable(gitHubProperty).map(GitHubProperty::getToken).orElse(EMPTY).trim()) - .build(); + return new GitHubBuilder().withOAuthToken( + Optional.ofNullable(gitHubProperty).map(GitHubProperty::getToken).orElse(EMPTY).trim()).build(); } @Override @@ -109,6 +111,8 @@ public GitHubAccessTokenResponse getAccessToken(String code, GitHubProperty gitH GitHubAccessTokenResponse response = responseEntity.getBody(); if (response != null && response.getError() != null && !response.getError().isBlank()) { + log.error(String.format(ErrorMessageConstants.CURRENT_CLIENT_ID_MISMATCH_MESSAGE, code, + gitHubProperty.getOauth2ClientId())); throw new Oauth2ExchangeCodeException(response.getError(), response.getErrorDescription()); } @@ -175,8 +179,8 @@ public List> getUserOrganizations(String accessToken) throws return response.getBody(); } catch (HttpClientErrorException exception) { throw new UnauthorizedException(ErrorCode.GITHUB_USER_UNAUTHORIZED.getCode(), - ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText() + "-" + GitHubUtils.extractMessageFromExceptionMessage( - exception.getMessage())); + ErrorCode.GITHUB_USER_UNAUTHORIZED.getHelpText() + CommonConstants.DASH_SEPARATOR + + GitHubUtils.extractMessageFromExceptionMessage(exception.getMessage())); } } } 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 7c982cb1f..6d72dab73 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 @@ -15,6 +15,8 @@ import java.util.List; import java.util.stream.Collectors; +import static com.axonivy.market.constants.MetaConstants.META_FILE; + @Log4j2 @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GitHubUtils { @@ -37,7 +39,7 @@ public static String getDownloadUrl(GHContent content) { } catch (IOException e) { log.error("Cannot get DownloadURl from GHContent: ", e); } - return ""; + return StringUtils.EMPTY; } public static List mapPagedIteratorToList(PagedIterable paged) { @@ -79,15 +81,23 @@ public static String extractMessageFromExceptionMessage(String exceptionMessage) return json.substring(startIndex, endIndex); } } - return ""; + return StringUtils.EMPTY; } - private static String extractJson(String text) { + public static String extractJson(String text) { int start = text.indexOf("{"); int end = text.lastIndexOf("}") + 1; if (start != -1 && end != -1) { return text.substring(start, end); } - return ""; + return StringUtils.EMPTY; + } + + public static int sortMetaJsonFirst(String fileName1, String fileName2) { + if (fileName1.endsWith(META_FILE)) + return -1; + if (fileName2.endsWith(META_FILE)) + return 1; + return fileName1.compareTo(fileName2); } } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductModuleContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductModuleContentRepository.java new file mode 100644 index 000000000..af4652af8 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomProductModuleContentRepository.java @@ -0,0 +1,8 @@ +package com.axonivy.market.repository; + +import java.util.List; + +public interface CustomProductModuleContentRepository { + + List findTagsByProductId(String id); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/CustomRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomRepository.java new file mode 100644 index 000000000..1de688ba5 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/CustomRepository.java @@ -0,0 +1,25 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.constants.MongoDBConstants; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +public class CustomRepository { + protected AggregationOperation createIdMatchOperation(String id) { + return createFieldMatchOperation(MongoDBConstants.ID, id); + } + + protected Query createQueryById(String id) { + return new Query(Criteria.where(MongoDBConstants.ID).is(id)); + } + + protected AggregationOperation createProjectAggregationBySingleFieldName(String fieldName) { + return Aggregation.project(fieldName); + } + + protected AggregationOperation createFieldMatchOperation(String fieldName, String id) { + return Aggregation.match(Criteria.where(fieldName).is(id)); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java new file mode 100644 index 000000000..4949a1d2c --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ImageRepository.java @@ -0,0 +1,12 @@ +package com.axonivy.market.repository; + +import com.axonivy.market.entity.Image; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ImageRepository extends MongoRepository { + Image findByProductIdAndSha(String productId, String sha); + + void deleteAllByProductId(String productId); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java index d803d824b..9d8dbfc7a 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductJsonContentRepository.java @@ -8,7 +8,4 @@ public interface ProductJsonContentRepository extends MongoRepository { ProductJsonContent findByProductIdAndVersion(String productId , String version); - - boolean existsByProductIdAndVersion(String productId , String version); - } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java index 75bdc53db..123a30c1c 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/ProductModuleContentRepository.java @@ -5,6 +5,6 @@ import org.springframework.stereotype.Repository; @Repository -public interface ProductModuleContentRepository extends MongoRepository { - ProductModuleContent findByTagAndProductId(String tag, String productId); +public interface ProductModuleContentRepository extends MongoRepository, CustomProductModuleContentRepository { + ProductModuleContent findByTagAndProductId(String tag, String productId); } 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 c138cadf4..946a87bc0 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 @@ -10,4 +10,6 @@ public interface ProductRepository extends MongoRepository, Pro Product findByLogoUrl(String logoUrl); + Product findByLogoId(String logoId); + } diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductModuleContentRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductModuleContentRepositoryImpl.java new file mode 100644 index 000000000..933b6a366 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductModuleContentRepositoryImpl.java @@ -0,0 +1,34 @@ +package com.axonivy.market.repository.impl; + +import com.axonivy.market.constants.EntityConstants; +import com.axonivy.market.constants.MongoDBConstants; +import com.axonivy.market.entity.ProductModuleContent; +import com.axonivy.market.repository.CustomProductModuleContentRepository; +import com.axonivy.market.repository.CustomRepository; +import lombok.Builder; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; + +import java.util.List; + +@Builder +public class CustomProductModuleContentRepositoryImpl extends CustomRepository implements CustomProductModuleContentRepository { + + private final MongoTemplate mongoTemplate; + + public CustomProductModuleContentRepositoryImpl(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @Override + public List findTagsByProductId(String id) { + Aggregation aggregation = Aggregation.newAggregation(createFieldMatchOperation(MongoDBConstants.PRODUCT_ID, id), + createProjectAggregationBySingleFieldName(MongoDBConstants.TAG)); + return queryProductModuleContentsByAggregation(aggregation).stream().map(ProductModuleContent::getTag).toList(); + } + + public List queryProductModuleContentsByAggregation(Aggregation aggregation) { + return mongoTemplate.aggregate(aggregation, EntityConstants.PRODUCT_MODULE_CONTENT, ProductModuleContent.class) + .getMappedResults(); + } +} \ No newline at end of file diff --git a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java index efb1ef4be..a91d50200 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java +++ b/marketplace-service/src/main/java/com/axonivy/market/repository/impl/CustomProductRepositoryImpl.java @@ -1,16 +1,17 @@ package com.axonivy.market.repository.impl; +import com.axonivy.market.constants.EntityConstants; import com.axonivy.market.constants.MongoDBConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.entity.ProductModuleContent; import com.axonivy.market.entity.ProductDesignerInstallation; import com.axonivy.market.repository.CustomProductRepository; +import com.axonivy.market.repository.CustomRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import lombok.Builder; import org.springframework.data.mongodb.core.FindAndModifyOptions; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; -import org.springframework.data.mongodb.core.aggregation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; @@ -22,7 +23,7 @@ import java.util.Optional; @Builder -public class CustomProductRepositoryImpl implements CustomProductRepository { +public class CustomProductRepositoryImpl extends CustomRepository implements CustomProductRepository { private final MongoTemplate mongoTemplate; private final ProductModuleContentRepository contentRepository; @@ -31,12 +32,9 @@ public CustomProductRepositoryImpl(MongoTemplate mongoTemplate, ProductModuleCon this.contentRepository = contentRepository; } - private AggregationOperation createIdMatchOperation(String id) { - return Aggregation.match(Criteria.where(MongoDBConstants.ID).is(id)); - } public Product queryProductByAggregation(Aggregation aggregation) { - return Optional.of(mongoTemplate.aggregate(aggregation, MongoDBConstants.PRODUCT_COLLECTION, Product.class)) + return Optional.of(mongoTemplate.aggregate(aggregation, EntityConstants.PRODUCT, Product.class)) .map(AggregationResults::getUniqueMappedResult).orElse(null); } @@ -90,9 +88,6 @@ public int increaseInstallationCount(String productId) { return updatedProduct != null ? updatedProduct.getInstallationCount() : 0; } - private Query createQueryById(String id) { - return new Query(Criteria.where(MongoDBConstants.ID).is(id)); - } @Override public void increaseInstallationCountForProductByDesignerVersion(String productId, String designerVersion) { diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java new file mode 100644 index 000000000..f87dbf830 --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/ImageService.java @@ -0,0 +1,15 @@ +package com.axonivy.market.service; + +import org.bson.types.Binary; +import org.kohsuke.github.GHContent; + +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; + +public interface ImageService { + Binary getImageBinary(GHContent ghContent); + + Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo); + + byte[] readImage(String id); +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java new file mode 100644 index 000000000..1b589872f --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/service/impl/ImageServiceImpl.java @@ -0,0 +1,65 @@ +package com.axonivy.market.service.impl; + +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; +import com.axonivy.market.github.util.GitHubUtils; +import com.axonivy.market.repository.ImageRepository; +import com.axonivy.market.service.ImageService; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.bson.types.Binary; +import org.kohsuke.github.GHContent; +import org.springframework.stereotype.Service; + +import java.io.InputStream; + +@Service +@Log4j2 +public class ImageServiceImpl implements ImageService { + + private final ImageRepository imageRepository; + + public ImageServiceImpl(ImageRepository imageRepository) { + this.imageRepository = imageRepository; + } + + @Override + public Binary getImageBinary(GHContent ghContent) { + try { + InputStream contentStream = ghContent.read(); + byte[] sourceBytes = IOUtils.toByteArray(contentStream); + return new Binary(sourceBytes); + } catch (Exception exception) { + log.error("Cannot get content of product logo {} ", ghContent.getName()); + return null; + } + } + + @Override + public Image mappingImageFromGHContent(Product product, GHContent ghContent, boolean isLogo) { + if (ObjectUtils.isEmpty(ghContent)) { + log.info("There is missing for image content for product {}" , product.getId()); + return null; + } + + if (!isLogo) { + Image existsImage = imageRepository.findByProductIdAndSha(product.getId(), ghContent.getSha()); + if (ObjectUtils.isNotEmpty(existsImage)) { + return existsImage; + } + } + Image image = new Image(); + String currentImageUrl = GitHubUtils.getDownloadUrl(ghContent); + image.setProductId(product.getId()); + image.setImageUrl(currentImageUrl); + image.setImageData(getImageBinary(ghContent)); + image.setSha(ghContent.getSha()); + return imageRepository.save(image); + } + + @Override + public byte[] readImage(String id) { + return imageRepository.findById(id).map(Image::getImageData).map(Binary::getData).orElse(null); + } +} 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 afdfd538b..6c2c7ffb9 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 @@ -1,40 +1,6 @@ package com.axonivy.market.service.impl; -import static com.axonivy.market.enums.DocumentField.MARKET_DIRECTORY; -import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; - -import static java.util.Optional.ofNullable; -import static org.apache.commons.lang3.StringUtils.EMPTY; - -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.SecureRandom; -import java.util.*; - import com.axonivy.market.comparator.MavenVersionComparator; -import com.axonivy.market.util.VersionUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.ObjectUtils; -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.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -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.constants.ProductJsonConstants; @@ -57,14 +23,55 @@ import com.axonivy.market.github.util.GitHubUtils; import com.axonivy.market.model.ProductCustomSortRequest; import com.axonivy.market.repository.GitHubRepoMetaRepository; +import com.axonivy.market.repository.ImageRepository; import com.axonivy.market.repository.ProductCustomSortRepository; import com.axonivy.market.repository.ProductModuleContentRepository; import com.axonivy.market.repository.ProductRepository; +import com.axonivy.market.service.ImageService; import com.axonivy.market.service.ProductService; +import com.axonivy.market.util.VersionUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +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.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; +import static com.axonivy.market.enums.DocumentField.MARKET_DIRECTORY; +import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.StringUtils.EMPTY; @Log4j2 @Service @@ -77,7 +84,9 @@ public class ProductServiceImpl implements ProductService { private final GitHubRepoMetaRepository gitHubRepoMetaRepository; private final GitHubService gitHubService; private final ProductCustomSortRepository productCustomSortRepository; + private final ImageRepository imageRepository; + private final ImageService imageService; private final MongoTemplate mongoTemplate; private GHCommit lastGHCommit; @@ -96,10 +105,10 @@ public class ProductServiceImpl implements ProductService { public ProductServiceImpl(ProductRepository productRepository, ProductModuleContentRepository productModuleContentRepository, - GHAxonIvyMarketRepoService axonIvyMarketRepoService, - GHAxonIvyProductRepoService axonIvyProductRepoService, GitHubRepoMetaRepository gitHubRepoMetaRepository, - GitHubService gitHubService, ProductCustomSortRepository productCustomSortRepository, - MongoTemplate mongoTemplate) { + GHAxonIvyMarketRepoService axonIvyMarketRepoService, GHAxonIvyProductRepoService axonIvyProductRepoService, + GitHubRepoMetaRepository gitHubRepoMetaRepository, GitHubService gitHubService, + ProductCustomSortRepository productCustomSortRepository, ImageRepository imageRepository1, + ImageService imageService, MongoTemplate mongoTemplate) { this.productRepository = productRepository; this.productModuleContentRepository = productModuleContentRepository; this.axonIvyMarketRepoService = axonIvyMarketRepoService; @@ -107,6 +116,8 @@ public ProductServiceImpl(ProductRepository productRepository, this.gitHubRepoMetaRepository = gitHubRepoMetaRepository; this.gitHubService = gitHubService; this.productCustomSortRepository = productCustomSortRepository; + this.imageRepository = imageRepository1; + this.imageService = imageService; this.mongoTemplate = mongoTemplate; } @@ -198,7 +209,7 @@ private void syncRepoMetaDataStatus() { private void updateLatestChangeToProductsFromGithubRepo() { var fromSHA1 = marketRepoMeta.getLastSHA1(); - var toSHA1 = ofNullable(lastGHCommit).map(GHCommit::getSHA1).orElse(""); + var toSHA1 = ofNullable(lastGHCommit).map(GHCommit::getSHA1).orElse(EMPTY); log.warn("**ProductService: synchronize products from SHA1 {} to SHA1 {}", fromSHA1, toSHA1); List gitHubFileChanges = axonIvyMarketRepoService.fetchMarketItemsBySHA1Range(fromSHA1, toSHA1); Map> groupGitHubFiles = new HashMap<>(); @@ -206,6 +217,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { String filePath = file.getFileName(); var parentPath = filePath.replace(FileType.META.getFileName(), EMPTY).replace(FileType.LOGO.getFileName(), EMPTY); var files = groupGitHubFiles.getOrDefault(parentPath, new ArrayList<>()); + files.sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getFileName(), file2.getFileName())); files.add(file); groupGitHubFiles.putIfAbsent(parentPath, files); } @@ -224,6 +236,7 @@ private void updateLatestChangeToProductsFromGithubRepo() { ProductFactory.mappingByGHContent(product, fileContent); if (FileType.META == file.getType()) { + transferComputedDataFromDB(product); modifyProductByMetaContent(file, product); } else { modifyProductLogo(ghFileEntity.getKey(), file, product, fileContent); @@ -232,6 +245,10 @@ private void updateLatestChangeToProductsFromGithubRepo() { }); } + private static Predicate filterNonPersistGhTagName(List currentTags) { + return tag -> !currentTags.contains(tag.getName()); + } + private void modifyProductLogo(String parentPath, GitHubFile file, Product product, GHContent fileContent) { Product result; switch (file.getStatus()) { @@ -241,13 +258,17 @@ private void modifyProductLogo(String parentPath, GitHubFile file, Product produ searchCriteria.setFields(List.of(MARKET_DIRECTORY)); result = productRepository.findByCriteria(searchCriteria); if (result != null) { - result.setLogoUrl(GitHubUtils.getDownloadUrl(fileContent)); - productRepository.save(result); + Optional.ofNullable(imageService.mappingImageFromGHContent(result, fileContent, true)).ifPresent(image -> { + imageRepository.deleteById(result.getLogoId()); + result.setLogoId(image.getId()); + productRepository.save(result); + }); } break; case REMOVED: - result = productRepository.findByLogoUrl(product.getLogoUrl()); + result = productRepository.findByLogoId(product.getLogoId()); if (result != null) { + imageRepository.deleteAllByProductId(result.getId()); productRepository.deleteById(result.getId()); } break; @@ -256,19 +277,6 @@ private void modifyProductLogo(String parentPath, GitHubFile file, Product produ } } - 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; - } - } - private Pageable refinePagination(String language, Pageable pageable) { PageRequest pageRequest = (PageRequest) pageable; if (pageable != null) { @@ -317,6 +325,19 @@ private boolean isLastGithubCommitCovered() { return isLastCommitCovered; } + 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; + } + } + private void updateLatestReleaseTagContentsFromProductRepo() { List products = productRepository.findAll(); if (ObjectUtils.isEmpty(products)) { @@ -331,28 +352,11 @@ private void updateLatestReleaseTagContentsFromProductRepo() { } } - private void syncProductsFromGitHubRepo() { - log.warn("**ProductService: synchronize products from scratch based on the Market repo"); - var gitHubContentMap = axonIvyMarketRepoService.fetchAllMarketItems(); - gitHubContentMap.entrySet().forEach(ghContentEntity -> { - Product product = new Product(); - for (var content : ghContentEntity.getValue()) { - ProductFactory.mappingByGHContent(product, content); - } - if (StringUtils.isNotBlank(product.getRepositoryName())) { - updateProductCompatibility(product); - getProductContents(product); - } else { - updateProductContentForNonStandardProduct(ghContentEntity, product); - } - productRepository.save(product); - }); - } - private void updateProductContentForNonStandardProduct(Map.Entry> ghContentEntity, Product product) { ProductModuleContent initialContent = new ProductModuleContent(); initialContent.setTag(INITIAL_VERSION); initialContent.setProductId(product.getId()); + ProductFactory.mappingIdForProductModuleContent(initialContent); product.setReleasedVersions(List.of(INITIAL_VERSION)); product.setNewestReleaseVersion(INITIAL_VERSION); axonIvyProductRepoService.extractReadMeFileFromContents(product, ghContentEntity.getValue(), initialContent); @@ -368,6 +372,39 @@ private void getProductContents(Product product) { } } + private void syncProductsFromGitHubRepo() { + log.warn("**ProductService: synchronize products from scratch based on the Market repo"); + var gitHubContentMap = axonIvyMarketRepoService.fetchAllMarketItems(); + for (Map.Entry> ghContentEntity : gitHubContentMap.entrySet()) { + Product product = new Product(); + //update the meta.json first + ghContentEntity.getValue() + .sort((file1, file2) -> GitHubUtils.sortMetaJsonFirst(file1.getName(), file2.getName())); + for (var content : ghContentEntity.getValue()) { + ProductFactory.mappingByGHContent(product, content); + mappingLogoFromGHContent(product, content); + } + if (productRepository.findById(product.getId()).isPresent()) { + continue; + } + if (StringUtils.isNotBlank(product.getRepositoryName())) { + updateProductCompatibility(product); + getProductContents(product); + } else { + updateProductContentForNonStandardProduct(ghContentEntity, product); + } + transferComputedDataFromDB(product); + productRepository.save(product); + } + } + + private void mappingLogoFromGHContent(Product product, GHContent ghContent) { + if (StringUtils.endsWith(ghContent.getName(), LOGO_FILE)) { + Optional.ofNullable(imageService.mappingImageFromGHContent(product, ghContent, true)) + .ifPresent(image -> product.setLogoId(image.getId())); + } + } + private void updateProductFromReleaseTags(Product product, GHRepository productRepo) { List productModuleContents = new ArrayList<>(); List ghTags = getProductReleaseTags(product); @@ -377,11 +414,11 @@ private void updateProductFromReleaseTags(Product product, GHRepository productR } product.setNewestPublishedDate(getPublishedDateFromLatestTag(lastTag)); product.setNewestReleaseVersion(lastTag.getName()); - - if (!CollectionUtils.isEmpty(product.getReleasedVersions())) { - List currentTags = VersionUtils.getReleaseTagsFromProduct(product); - ghTags = ghTags.stream().filter(t -> !currentTags.contains(t.getName())).toList(); + List currentTags = VersionUtils.getReleaseTagsFromProduct(product); + if (CollectionUtils.isEmpty(currentTags)) { + currentTags = productModuleContentRepository.findTagsByProductId(product.getId()); } + ghTags = ghTags.stream().filter(filterNonPersistGhTagName(currentTags)).toList(); for (GHTag ghTag : ghTags) { ProductModuleContent productModuleContent = @@ -510,6 +547,7 @@ public List refineOrderedListOfProductsInCustomSort(List ordere } Product product = productOptional.get(); product.setCustomOrder(descendingOrder--); + productRepository.save(product); productEntries.add(product); } @@ -520,4 +558,11 @@ public void removeFieldFromAllProductDocuments(String fieldName) { Update update = new Update().unset(fieldName); mongoTemplate.updateMulti(new Query(), update, Product.class); } + + public void transferComputedDataFromDB(Product product) { + productRepository.findById(product.getId()).ifPresent(persistedData -> + ProductFactory.transferComputedPersistedDataToProduct(persistedData, product) + ); + } + } 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 2f0a81e8d..556bb8e8c 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 @@ -48,6 +48,7 @@ import static com.axonivy.market.constants.ProductJsonConstants.NAME; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + @Log4j2 @Service @Getter @@ -96,7 +97,8 @@ public List getArtifactsAndVersionToDisplay(String pr this.productId = productId; artifactsFromMeta = getProductMetaArtifacts(productId); - List versionsToDisplay = VersionUtils.getVersionsToDisplay(getVersionsFromMavenArtifacts(), isShowDevVersion, designerVersion); + List versionsToDisplay = VersionUtils.getVersionsToDisplay(getVersionsFromMavenArtifacts(), + isShowDevVersion, designerVersion); proceedDataCache = mavenArtifactVersionRepository.findById(productId).orElse(new MavenArtifactVersion(productId)); metaProductArtifact = artifactsFromMeta.stream() .filter(artifact -> artifact.getArtifactId().endsWith(MavenConstants.PRODUCT_ARTIFACT_POSTFIX)).findAny() @@ -112,17 +114,18 @@ public List getArtifactsAndVersionToDisplay(String pr } @Override - public Map getProductJsonContentByIdAndVersion(String productId, String version){ + public Map getProductJsonContentByIdAndVersion(String productId, String version) { Map result = new HashMap<>(); try { - ProductJsonContent productJsonContent = productJsonContentRepository.findByProductIdAndVersion(productId, version); + ProductJsonContent productJsonContent = productJsonContentRepository.findByProductIdAndVersion(productId, + version); if (ObjectUtils.isEmpty(productJsonContent)) { return new HashMap<>(); } result = mapper.readValue(productJsonContent.getContent(), Map.class); result.computeIfAbsent(NAME, k -> productJsonContent.getName()); - } catch (JsonProcessingException jsonProcessingException){ + } catch (JsonProcessingException jsonProcessingException) { log.error(jsonProcessingException.getMessage()); } return result; @@ -249,7 +252,9 @@ public String getVersionTag(String version) { public String buildProductJsonFilePath() { String pathToProductFolderFromTagContent = metaProductArtifact.getArtifactId(); - GitHubUtils.getNonStandardProductFilePath(productId); + if (NonStandardProduct.DEFAULT != NonStandardProduct.findById(productId)) { + pathToProductFolderFromTagContent = GitHubUtils.getNonStandardProductFilePath(productId); + } productJsonFilePath = String.format(GitHubConstants.PRODUCT_JSON_FILE_PATH_FORMAT, pathToProductFolderFromTagContent); return productJsonFilePath; diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java new file mode 100644 index 000000000..ddab4a2fe --- /dev/null +++ b/marketplace-service/src/main/java/com/axonivy/market/util/ImageUtils.java @@ -0,0 +1,60 @@ +package com.axonivy.market.util; + +import static com.axonivy.market.constants.CommonConstants.IMAGE_ID_PREFIX; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.axonivy.market.controller.ImageController; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.util.Strings; +import org.springframework.hateoas.Link; +import com.axonivy.market.entity.ProductModuleContent; + +public class ImageUtils { + public static final String IMAGE_ID_FORMAT_PATTERN = "imageId-\\w+"; + private static final Pattern PATTERN = Pattern.compile(IMAGE_ID_FORMAT_PATTERN); + + private ImageUtils() { + } + + public static ProductModuleContent mappingImageForProductModuleContent(ProductModuleContent productModuleContent) { + if (ObjectUtils.isEmpty(productModuleContent)) { + return null; + } + mappingImageUrl(productModuleContent.getDescription()); + mappingImageUrl(productModuleContent.getDemo()); + mappingImageUrl(productModuleContent.getSetup()); + return productModuleContent; + } + + private static void mappingImageUrl(Map content) { + if (ObjectUtils.isEmpty(content)) { + return; + } + content.forEach((key, value) -> { + List imageIds = extractAllImageIds(value); + for (String imageId : imageIds) { + String rawId = imageId.replace(IMAGE_ID_PREFIX, Strings.EMPTY); + Link link = linkTo(methodOn(ImageController.class).findImageById(rawId)).withSelfRel(); + value = value.replace(imageId, link.getHref()); + } + content.put(key, value); + }); + } + + private static List extractAllImageIds(String content) { + List result = new ArrayList<>(); + Matcher matcher = PATTERN.matcher(content); + while (matcher.find()) { + var foundImgTag = matcher.group(); + result.add(foundImgTag); + } + return result; + } +} diff --git a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java index 55d48e03e..ea4bb7963 100644 --- a/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java +++ b/marketplace-service/src/main/java/com/axonivy/market/util/VersionUtils.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.stream.Stream; public class VersionUtils { @@ -126,7 +125,7 @@ public static String getOldestVersion(List tags) { return result; } public static List getReleaseTagsFromProduct(Product product) { - if (Objects.isNull(product)) { + if (Objects.isNull(product) || CollectionUtils.isEmpty(product.getReleasedVersions())) { return new ArrayList<>(); } return product.getReleasedVersions().stream().map(version -> convertVersionToTag(product.getId(), version)).toList(); diff --git a/marketplace-service/src/main/resources/application.properties b/marketplace-service/src/main/resources/application.properties index 743162dde..9dc5a98cd 100644 --- a/marketplace-service/src/main/resources/application.properties +++ b/marketplace-service/src/main/resources/application.properties @@ -3,18 +3,17 @@ spring.data.mongodb.uri=mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@${MONG spring.data.mongodb.database=${MONGODB_DATABASE} server.port=8080 logging.level.org.springframework.web=warn -request.header=ivy server.forward-headers-strategy=framework springdoc.api-docs.path=/api-docs springdoc.swagger-ui.path=/swagger-ui.html market.cors.allowed.origin.maxAge=3600 market.cors.allowed.origin.patterns=${MARKET_CORS_ALLOWED_ORIGIN} -synchronized.installation.counts.path=${SYNCHRONIZED_INSTALLATION_COUNTS_PATH} +synchronized.installation.counts.path=/data/market-installation.json market.github.market.branch=${MARKET_GITHUB_MARKET_BRANCH} market.github.token=${MARKET_GITHUB_TOKEN} market.github.oauth2-clientId=${MARKET_GITHUB_OAUTH_APP_CLIENT_ID} market.github.oauth2-clientSecret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET} jwt.secret=${MARKET_JWT_SECRET_KEY} jwt.expiration=365 -logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG +logging.level.org.springframework.data.mongodb.core.MongoTemplate=${MARKET_MONGO_LOG_LEVEL} spring.jackson.serialization.indent_output=true \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java index 366c4962e..75c2566b5 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/assembler/ProductDetailModelAssemblerTest.java @@ -54,6 +54,6 @@ void testToModelWithRequestPathAndVersion() { void testToModelWithRequestPathAndBestMatchVersion() { ProductDetailModel model = productDetailModelAssembler.toModel(mockProduct, VERSION, RequestMappingConstants.BEST_MATCH_BY_ID_AND_VERSION); Assertions.assertTrue(model.getLink(SELF_RELATION).get().getHref().endsWith("/api/product-details/portal/10.0.19/bestmatch")); - Assertions.assertTrue(model.getMetaProductJsonUrl().endsWith("/api/product-details/productjsoncontent/portal/10.0.8")); + Assertions.assertTrue(model.getMetaProductJsonUrl().endsWith("/api/product-details/portal/10.0.8/json")); } } \ No newline at end of file diff --git a/marketplace-service/src/test/java/com/axonivy/market/controller/ImageControllerTest.java b/marketplace-service/src/test/java/com/axonivy/market/controller/ImageControllerTest.java new file mode 100644 index 000000000..295d8aeff --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/ImageControllerTest.java @@ -0,0 +1,40 @@ +package com.axonivy.market.controller; + +import com.axonivy.market.service.ImageService; +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.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ImageControllerTest { + + @Mock + private ImageService imageService; + + @InjectMocks + private ImageController imageController; + + @Test + void test_getImageFromId() { + byte[] mockImageData = "image data".getBytes(); + when(imageService.readImage("66e2b14868f2f95b2f95549a")).thenReturn(mockImageData); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_PNG); + ResponseEntity expectedResult = new ResponseEntity<>(mockImageData, headers, HttpStatus.OK); + + ResponseEntity result = imageController.findImageById("66e2b14868f2f95b2f95549a"); + + assertEquals(expectedResult, result); + } + +} 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 index cc4ae1bf8..5e3c04f55 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/controller/OAuth2ControllerTest.java @@ -1,26 +1,28 @@ package com.axonivy.market.controller; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.util.Map; - +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.entity.User; +import com.axonivy.market.exceptions.model.MissingHeaderException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.github.model.GitHubAccessTokenResponse; +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.HttpStatus; import org.springframework.http.ResponseEntity; -import com.axonivy.market.entity.User; -import com.axonivy.market.exceptions.model.MissingHeaderException; -import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; -import com.axonivy.market.github.model.GitHubAccessTokenResponse; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.model.Oauth2AuthorizationCode; -import com.axonivy.market.service.JwtService; +import java.util.Map; + +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 OAuth2ControllerTest { @@ -43,7 +45,7 @@ void setup() { } @Test - void testGitHubLogin() throws Oauth2ExchangeCodeException, MissingHeaderException { + void testGitHubLogin_Success() throws Oauth2ExchangeCodeException, MissingHeaderException { String accessToken = "sampleAccessToken"; User user = createUserMock(); String jwtToken = "sampleJwtToken"; @@ -58,6 +60,42 @@ void testGitHubLogin() throws Oauth2ExchangeCodeException, MissingHeaderExceptio assertEquals(Map.of("token", jwtToken), response.getBody()); } + @Test + void testGitHubLogin_Oauth2ExchangeCodeException() throws Oauth2ExchangeCodeException, MissingHeaderException { + when(gitHubService.getAccessToken(any(), any())).thenThrow( + new Oauth2ExchangeCodeException("invalid_grant", "Invalid authorization code")); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertEquals("invalid_grant", body.get(CommonConstants.ERROR)); + assertEquals("Invalid authorization code", body.get(CommonConstants.MESSAGE)); + } + + @Test + void testGitHubLogin_GeneralException() throws Oauth2ExchangeCodeException, MissingHeaderException { + when(gitHubService.getAccessToken(any(), any())).thenThrow(new RuntimeException("Unexpected error")); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertTrue(body.containsKey(CommonConstants.MESSAGE)); + assertEquals("Unexpected error", body.get(CommonConstants.MESSAGE)); + } + + @Test + void testGitHubLogin_EmptyAuthorizationCode() { + oauth2AuthorizationCode.setCode(null); + + ResponseEntity response = oAuth2Controller.gitHubLogin(oauth2AuthorizationCode); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + Map body = (Map) response.getBody(); + assertTrue(body.containsKey(CommonConstants.MESSAGE)); + } + private User createUserMock() { User user = new User(); user.setId("userId"); @@ -73,4 +111,4 @@ private GitHubAccessTokenResponse createGitHubAccessTokenResponseMock() { gitHubAccessTokenResponse.setAccessToken("sampleAccessToken"); return gitHubAccessTokenResponse; } -} \ 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 05f37b299..42ae3db76 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 @@ -14,6 +14,7 @@ import com.axonivy.market.constants.RequestMappingConstants; import com.axonivy.market.entity.ProductJsonContent; import com.axonivy.market.model.VersionAndUrlModel; +import com.axonivy.market.service.ImageService; import com.axonivy.market.service.ProductDesignerInstallationService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; @@ -39,6 +40,9 @@ class ProductDetailsControllerTest { @Mock private ProductService productService; + @Mock + private ImageService imageService; + @Mock VersionService versionService; 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 8aaa42318..24aa148c7 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,6 +1,6 @@ package com.axonivy.market.factory; -import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; import com.axonivy.market.entity.Product; import com.axonivy.market.github.model.Meta; import org.junit.jupiter.api.Assertions; @@ -11,6 +11,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.List; import static com.axonivy.market.constants.CommonConstants.SLASH; import static com.axonivy.market.constants.MetaConstants.META_FILE; @@ -41,15 +42,14 @@ void testMappingByGHContent() throws IOException { } @Test - void testMappingLogo() throws IOException { + void testMappingLogo() { Product product = new Product(); GHContent content = mock(GHContent.class); - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); + when(content.getName()).thenReturn(ProductJsonConstants.LOGO_FILE); var result = ProductFactory.mappingByGHContent(product, content); assertNotEquals(null, result); - when(content.getName()).thenReturn(CommonConstants.LOGO_FILE); - when(content.getDownloadUrl()).thenReturn(DUMMY_LOGO_URL); + when(content.getName()).thenReturn(ProductJsonConstants.LOGO_FILE); result = ProductFactory.mappingByGHContent(product, content); assertNotEquals(null, result); } @@ -74,4 +74,20 @@ void testExtractSourceUrl() { Assertions.assertEquals(sourceUrl, product.getRepositoryName()); Assertions.assertEquals(sourceUrl, product.getSourceUrl()); } -} + + @Test + void testTransferComputedData() { + String initialVersion ="10.0.2"; + Product product = new Product(); + Product persistedData = new Product(); + persistedData.setCustomOrder(1); + persistedData.setReleasedVersions(List.of(initialVersion)); + persistedData.setNewestReleaseVersion(initialVersion); + + ProductFactory.transferComputedPersistedDataToProduct(persistedData,product); + assertEquals(1,product.getCustomOrder()); + assertEquals(initialVersion, product.getNewestReleaseVersion()); + assertEquals(1, product.getReleasedVersions().size()); + assertEquals(initialVersion, product.getReleasedVersions().get(0)); + } +} \ No newline at end of file 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 b5b16ed94..d440e0215 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 @@ -5,6 +5,8 @@ import com.axonivy.market.exceptions.model.MissingHeaderException; import com.axonivy.market.exceptions.model.NoContentException; import com.axonivy.market.exceptions.model.NotFoundException; +import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException; +import com.axonivy.market.exceptions.model.UnauthorizedException; import com.axonivy.market.model.Message; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,4 +63,18 @@ void testHandleInvalidException() { var responseEntity = exceptionHandlers.handleInvalidException(invalidParamException); assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); } + + @Test + void testHandleOauth2ExchangeCodeException() { + var oauth2ExchangeCodeException = mock(Oauth2ExchangeCodeException.class); + var responseEntity = exceptionHandlers.handleOauth2ExchangeCodeException(oauth2ExchangeCodeException); + assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); + } + + @Test + void testHandleUnauthorizedException() { + var unauthorizedException = mock(UnauthorizedException.class); + var responseEntity = exceptionHandlers.handleUnauthorizedException(unauthorizedException); + assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java index 8a58b28a3..b4ebfce5e 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/GHAxonIvyProductRepoServiceImplTest.java @@ -1,16 +1,26 @@ package com.axonivy.market.service.impl; -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.entity.ProductJsonContent; -import com.axonivy.market.enums.Language; -import com.axonivy.market.github.model.MavenArtifact; -import com.axonivy.market.github.service.GitHubService; -import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; -import com.axonivy.market.repository.ProductJsonContentRepository; -import com.fasterxml.jackson.databind.JsonNode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 java.util.Map; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,25 +36,19 @@ 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 java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.axonivy.market.constants.CommonConstants; +import com.axonivy.market.constants.ProductJsonConstants; +import com.axonivy.market.constants.ReadmeConstants; +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; +import com.axonivy.market.entity.ProductJsonContent; +import com.axonivy.market.enums.Language; +import com.axonivy.market.github.model.MavenArtifact; +import com.axonivy.market.github.service.GitHubService; +import com.axonivy.market.github.service.impl.GHAxonIvyProductRepoServiceImpl; +import com.axonivy.market.repository.ProductJsonContentRepository; +import com.axonivy.market.service.ImageService; +import com.fasterxml.jackson.databind.JsonNode; @ExtendWith(MockitoExtension.class) class GHAxonIvyProductRepoServiceImplTest { @@ -75,6 +79,9 @@ class GHAxonIvyProductRepoServiceImplTest { @Mock GHContent content = new GHContent(); + @Mock + ImageService imageService; + @Mock ProductJsonContentRepository productJsonContentRepository; @@ -210,12 +217,18 @@ void testGetOrganization() throws IOException { @Test void testGetReadmeAndProductContentsFromTag() throws IOException { String readmeContentWithImage = "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + testGetReadmeAndProductContentsFromTagWithReadmeText(readmeContentWithImage); + String readmeContentWithoutHashProductName = "Test README\n## Demo\nDemo content\n## Setup\nSetup content (image.png)"; + testGetReadmeAndProductContentsFromTagWithReadmeText(readmeContentWithoutHashProductName); + } + private void testGetReadmeAndProductContentsFromTagWithReadmeText(String readmeContentWithImage) throws IOException { GHContent mockContent = createMockProductFolderWithProductJson(); getReadmeInputStream(readmeContentWithImage, mockContent); InputStream inputStream = getMockInputStream(); Mockito.when(axonivyProductRepoServiceImpl.extractedContentStream(any())).thenReturn(inputStream); + Mockito.when(imageService.mappingImageFromGHContent(any(),any(),anyBoolean())).thenReturn(mockImage()); var result = axonivyProductRepoServiceImpl.getReadmeAndProductContentsFromTag(createMockProduct(), ghRepository, RELEASE_TAG); @@ -226,7 +239,17 @@ void testGetReadmeAndProductContentsFromTag() throws IOException { assertEquals("iar", result.getType()); assertEquals("Test README", result.getDescription().get(Language.EN.getValue())); assertEquals("Demo content", result.getDemo().get(Language.EN.getValue())); - assertEquals("Setup content (https://raw.githubusercontent.com/image.png)", result.getSetup().get(Language.EN.getValue())); + assertEquals("Setup content (imageId-66e2b14868f2f95b2f95549a)", result.getSetup().get(Language.EN.getValue())); + } + + public static Image mockImage() { + Image image = new Image(); + image.setId("66e2b14868f2f95b2f95549a"); + image.setSha("914d9b6956db7a1404622f14265e435f36db81fa"); + image.setProductId("amazon-comprehend"); + image.setImageUrl( + "https://raw.githubusercontent.com/amazon-comprehend-connector-product/images/comprehend-demo-sentiment.png"); + return image; } @Test @@ -236,8 +259,7 @@ void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException 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); - + Mockito.when(imageService.mappingImageFromGHContent(any(),any() ,anyBoolean())).thenReturn(mockImage()); PagedIterable pagedIterable = Mockito.mock(String.valueOf(GHContent.class)); when(mockImageFile.listDirectoryContent()).thenReturn(pagedIterable); when(pagedIterable.toList()).thenReturn(List.of(mockImageFile)); @@ -246,7 +268,7 @@ void testGetReadmeAndProductContentFromTag_ImageFromFolder() throws IOException List.of(mockImageFile), readmeContentWithImageFolder); assertEquals( - "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (https://raw.githubusercontent.com/image.png)", + "#Product-name\n Test README\n## Demo\nDemo content\n## Setup\nSetup content (imageId-66e2b14868f2f95b2f95549a)", updatedReadme); } @@ -400,11 +422,10 @@ private GHContent createMockProductFolderWithProductJson() throws IOException { return mockContent; } - private static GHContent createMockProductJson() throws IOException { + private static GHContent createMockProductJson() { 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/impl/ImageServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java new file mode 100644 index 000000000..9f95c35fa --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ImageServiceImplTest.java @@ -0,0 +1,72 @@ +package com.axonivy.market.service.impl; + +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.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHContent; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.axonivy.market.entity.Image; +import com.axonivy.market.entity.Product; +import com.axonivy.market.repository.ImageRepository; + +@ExtendWith(MockitoExtension.class) +class ImageServiceImplTest { + @InjectMocks + private ImageServiceImpl imageService; + + @Mock + private ImageRepository imageRepository; + + @Captor + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Image.class); + + @Test + void testMappingImageFromGHContent() throws IOException { + GHContent content = mock(GHContent.class); + when(content.getSha()).thenReturn("914d9b6956db7a1404622f14265e435f36db81fa"); + when(content.getDownloadUrl()).thenReturn("https://raw.githubusercontent.com/images/comprehend-demo-sentiment.png"); + + InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); + when(content.read()).thenReturn(inputStream); + + imageService.mappingImageFromGHContent(mockProduct(), content , true); + + Image expectedImage = new Image(); + expectedImage.setProductId("google-maps-connector"); + expectedImage.setSha("914d9b6956db7a1404622f14265e435f36db81fa"); + expectedImage.setImageUrl("https://raw.githubusercontent.com/images/comprehend-demo-sentiment.png"); + + verify(imageRepository).save(argumentCaptor.capture()); + + assertEquals(argumentCaptor.getValue().getProductId(),expectedImage.getProductId()); + assertEquals(argumentCaptor.getValue().getSha(),expectedImage.getSha()); + assertEquals(argumentCaptor.getValue().getImageUrl(),expectedImage.getImageUrl()); + } + + @Test + void testMappingImageFromGHContent_noGhContent() { + var result = imageService.mappingImageFromGHContent(mockProduct(), null , true); + assertNull(result); + } + + private Product mockProduct() { + return Product.builder().id("google-maps-connector") + .language("English") + .build(); + } +} diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java index 8c9e4e1ac..2f466ab8b 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/ProductServiceImplTest.java @@ -1,6 +1,6 @@ package com.axonivy.market.service.impl; -import static com.axonivy.market.constants.CommonConstants.LOGO_FILE; +import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; import static com.axonivy.market.constants.CommonConstants.SLASH; import static com.axonivy.market.constants.MetaConstants.META_FILE; import static com.axonivy.market.enums.DocumentField.SHORT_DESCRIPTIONS; @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; @@ -34,6 +35,7 @@ import java.util.UUID; import com.axonivy.market.criteria.ProductSearchCriteria; +import com.axonivy.market.service.ImageService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,6 +47,7 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -130,6 +133,9 @@ class ProductServiceImplTest extends BaseSetup { @Mock private GHAxonIvyProductRepoService ghAxonIvyProductRepoService; + @Mock + private ImageService imageService; + @Captor ArgumentCaptor> productListArgumentCaptor; @@ -273,7 +279,6 @@ void testSyncProductsAsUpdateLogoFromGitHub() throws IOException { mockGitHubFile.setStatus(FileStatus.REMOVED); when(marketRepoService.fetchMarketItemsBySHA1Range(any(), any())).thenReturn(List.of(mockGitHubFile)); when(gitHubService.getGHContent(any(), anyString(), any())).thenReturn(mockGHContent); - when(productRepository.findByLogoUrl(any())).thenReturn(new Product()); // Executes result = productService.syncLatestDataFromMarketRepo(); @@ -332,18 +337,25 @@ void testSyncProductsFirstTime() throws IOException { var mockContent = mockGHContentAsMetaJSON(); InputStream inputStream = this.getClass().getResourceAsStream(SLASH.concat(META_FILE)); when(mockContent.read()).thenReturn(inputStream); + + var mockContentLogo = mockGHContentAsLogo(); + List mockMetaJsonAndLogoList = new ArrayList<>(List.of(mockContent, mockContentLogo)); + Map> mockGHContentMap = new HashMap<>(); - mockGHContentMap.put(SAMPLE_PRODUCT_ID, List.of(mockContent)); + mockGHContentMap.put(SAMPLE_PRODUCT_ID, mockMetaJsonAndLogoList); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); when(productModuleContentRepository.saveAll(anyList())).thenReturn(List.of(mockReadmeProductContent())); + Mockito.when(imageService.mappingImageFromGHContent(any(), any(), anyBoolean())) + .thenReturn(GHAxonIvyProductRepoServiceImplTest.mockImage()); // Executes productService.syncLatestDataFromMarketRepo(); verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); verify(productRepository).save(argumentCaptor.capture()); - assertThat(argumentCaptorProductModuleContents.getValue()).usingRecursiveComparison() - .isEqualTo(List.of(mockReadmeProductContent())); + assertEquals(1, argumentCaptorProductModuleContents.getValue().size()); + assertThat(argumentCaptorProductModuleContents.getValue().get(0).getId()) + .isEqualTo(mockReadmeProductContent().getId()); } @Test @@ -356,10 +368,13 @@ void testSyncProductsFirstTimeWithOutSourceUrl() throws IOException { InputStream inputStream = this.getClass().getResourceAsStream(EMPTY_SOURCE_URL_META_JSON_FILE); when(mockContent.read()).thenReturn(inputStream); + var mockContentLogo = mockGHContentAsLogo(); + Map> mockGHContentMap = new HashMap<>(); - mockGHContentMap.put(SAMPLE_PRODUCT_ID, List.of(mockContent)); + List mockMetaJsonAndLogoList = new ArrayList<>(List.of(mockContent, mockContentLogo)); + mockGHContentMap.put(SAMPLE_PRODUCT_ID, mockMetaJsonAndLogoList); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); - + Mockito.when(imageService.mappingImageFromGHContent(any(),any(),anyBoolean())).thenReturn(GHAxonIvyProductRepoServiceImplTest.mockImage()); // Executes productService.syncLatestDataFromMarketRepo(); verify(productModuleContentRepository).save(argumentCaptorProductModuleContent.capture()); @@ -401,8 +416,8 @@ void testSyncProductsSecondTime() throws IOException { verify(productModuleContentRepository).saveAll(argumentCaptorProductModuleContents.capture()); verify(productRepository).save(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().getProductModuleContent()).usingRecursiveComparison() - .isEqualTo(mockReadmeProductContent()); + assertThat(argumentCaptor.getValue().getProductModuleContent().getId()) + .isEqualTo(mockReadmeProductContent().getId()); } @Test @@ -423,7 +438,7 @@ void testSyncNullProductModuleContent() { var mockCommit = mockGHCommitHasSHA1(SHA1_SAMPLE); when(marketRepoService.getLastCommit(anyLong())).thenReturn(mockCommit); when(repoMetaRepository.findByRepoName(anyString())).thenReturn(null); - + Map> mockGHContentMap = new HashMap<>(); mockGHContentMap.put(SAMPLE_PRODUCT_ID, new ArrayList<>()); when(marketRepoService.fetchAllMarketItems()).thenReturn(mockGHContentMap); @@ -601,9 +616,15 @@ private GHContent mockGHContentAsMetaJSON() { return mockGHContent; } + private GHContent mockGHContentAsLogo() { + var mockGHContent = mock(GHContent.class); + when(mockGHContent.getName()).thenReturn("logo.png"); + return mockGHContent; + } + private ProductModuleContent mockReadmeProductContent() { ProductModuleContent productModuleContent = new ProductModuleContent(); - productModuleContent.setId("123"); + productModuleContent.setId("amazon-comprehendv-10.0.2"); productModuleContent.setTag("v10.0.2"); productModuleContent.setName("Amazon Comprehend"); Map description = new HashMap<>(); diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java index 04cf00ce3..0e2b26bcd 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/SchedulingTasksTest.java @@ -14,7 +14,7 @@ @SpringBootTest(properties = { "MONGODB_USERNAME=user", "MONGODB_PASSWORD=password", "MONGODB_HOST=mongoHost", "MONGODB_DATABASE=product", "MARKET_GITHUB_OAUTH_APP_CLIENT_ID=clientId", "MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=clientSecret", "MARKET_JWT_SECRET_KEY=jwtSecret", - "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master" }) + "MARKET_CORS_ALLOWED_ORIGIN=*", "MARKET_GITHUB_MARKET_BRANCH=master", "MARKET_MONGO_LOG_LEVEL=DEBUG"}) class SchedulingTasksTest { @SpyBean diff --git a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java index e33f77ddf..a45e845fa 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/service/impl/VersionServiceImplTest.java @@ -425,11 +425,10 @@ void testGetVersionsForDesigner() { Assertions.assertEquals(result.stream().map(VersionAndUrlModel::getVersion).toList(), List.of("11.3.0", "11.1.1", "11.1.0", "10.0.2")); - Assertions.assertEquals("/api/product-details/productjsoncontent/11.3.0/11.3.0", result.get(0).getUrl()); - Assertions.assertEquals("/api/product-details/productjsoncontent/11.3.0/11.1.1", result.get(1).getUrl()); - Assertions.assertEquals("/api/product-details/productjsoncontent/11.3.0/11.1.0", result.get(2).getUrl()); - Assertions.assertEquals("/api/product-details/productjsoncontent/11.3.0/10.0.2", result.get(3).getUrl()); - + Assertions.assertEquals("/api/product-details/11.3.0/11.3.0/json", result.get(0).getUrl()); + Assertions.assertEquals("/api/product-details/11.3.0/11.1.1/json", result.get(1).getUrl()); + Assertions.assertEquals("/api/product-details/11.3.0/11.1.0/json", result.get(2).getUrl()); + Assertions.assertEquals("/api/product-details/11.3.0/10.0.2/json", result.get(3).getUrl()); } @Test 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 index 2f97404dd..376d90d6d 100644 --- a/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java +++ b/marketplace-service/src/test/java/com/axonivy/market/util/GitHubUtilsTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import static com.axonivy.market.constants.MetaConstants.META_FILE; +import static com.axonivy.market.constants.ProductJsonConstants.LOGO_FILE; + @ExtendWith(MockitoExtension.class) class GitHubUtilsTest { private static final String JIRA_CONNECTOR = "Jira Connector"; @@ -90,4 +93,52 @@ void testGetNonStandardImageFolder() { result = GitHubUtils.getNonStandardImageFolder(JIRA_CONNECTOR); Assertions.assertEquals("images", result); } + + @Test + void testSortMetaJsonFirst() { + int result = GitHubUtils.sortMetaJsonFirst(META_FILE, LOGO_FILE); + Assertions.assertEquals(-1, result); + + result = GitHubUtils.sortMetaJsonFirst(LOGO_FILE, META_FILE); + Assertions.assertEquals(1, result); + + result = GitHubUtils.sortMetaJsonFirst(LOGO_FILE, LOGO_FILE); + Assertions.assertEquals(0, result); + } + + @Test + void testExtractJson() { + // Test case: valid JSON inside a string + String exceptionMessage = "Error occurred: {\"message\":\"An error occurred\"}"; + String json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals("{\"message\":\"An error occurred\"}", json); + + // Test case: no JSON in string + exceptionMessage = "Error occurred: no json here"; + json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, json); + + // Test case: empty string + exceptionMessage = ""; + json = GitHubUtils.extractJson(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, json); + } + + @Test + void testExtractMessageFromExceptionMessage() { + // Test case: valid message extraction + String exceptionMessage = "Some error occurred: {\"message\":\"Invalid input data\"}"; + String extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals("Invalid input data", extractedMessage); + + // Test case: no message key + exceptionMessage = "Some error occurred: {\"error\":\"Something went wrong\"}"; + extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, extractedMessage); + + // Test case: empty exception message + exceptionMessage = ""; + extractedMessage = GitHubUtils.extractMessageFromExceptionMessage(exceptionMessage); + Assertions.assertEquals(StringUtils.EMPTY, extractedMessage); + } } diff --git a/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java b/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java new file mode 100644 index 000000000..07e7a218e --- /dev/null +++ b/marketplace-service/src/test/java/com/axonivy/market/util/ImageUtilsTest.java @@ -0,0 +1,41 @@ +package com.axonivy.market.util; + +import java.util.HashMap; +import java.util.Map; + +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; + +import com.axonivy.market.entity.ProductModuleContent; + +@ExtendWith(MockitoExtension.class) +class ImageUtilsTest { + + @Test + void testMappingImageForProductModuleContent() { + String expectedValue = "Login or create a new account.[demo-process](/api/image/66e2b13c68f2f95b2f95548c)"; + var result = ImageUtils.mappingImageForProductModuleContent(mockProductModuleContent()); + Assertions.assertEquals(expectedValue, result.getDescription().get("en")); + Assertions.assertEquals(expectedValue, result.getSetup().get("de")); + } + + private ProductModuleContent mockProductModuleContent(){ + ProductModuleContent productModuleContent = new ProductModuleContent(); + productModuleContent.setDescription(mockDescriptionForProductModuleContent()); + productModuleContent.setDemo(null); + productModuleContent.setSetup(mockDescriptionForProductModuleContent()); + + return productModuleContent; + } + + private Map mockDescriptionForProductModuleContent(){ + Map mutableMap = new HashMap<>(); + mutableMap.put("en", "Login or create a new account.[demo-process](imageId-66e2b13c68f2f95b2f95548c)"); + mutableMap.put("de", "Login or create a new account.[demo-process](imageId-66e2b13c68f2f95b2f95548c)"); + return mutableMap; + + } + +} diff --git a/marketplace-ui/src/app/app.component.html b/marketplace-ui/src/app/app.component.html index fb39f0b23..016a69a82 100644 --- a/marketplace-ui/src/app/app.component.html +++ b/marketplace-ui/src/app/app.component.html @@ -1,23 +1,30 @@ -@if(!routingQueryParamService.isDesignerEnv()){ -
-
- -
-
-} -
-
- -
-
-@if(!routingQueryParamService.isDesignerEnv()){ -
-
- -
-
-} +
+ @if (!routingQueryParamService.isDesignerEnv()) { +
+
+ +
+
+ } +
+
+ +
+
+ @if (!routingQueryParamService.isDesignerEnv()) { +
+
+ +
+
+ } -@if (loadingService.isLoading()) { - -} + @if (loadingService.isLoading()) { + + } +
diff --git a/marketplace-ui/src/app/app.component.scss b/marketplace-ui/src/app/app.component.scss index ad66cc73f..dba00e98b 100644 --- a/marketplace-ui/src/app/app.component.scss +++ b/marketplace-ui/src/app/app.component.scss @@ -24,3 +24,20 @@ footer { margin-top: 5%; } } + +.header-mobile { + z-index: 1; + position: absolute; + background: var(--info-dropdown-bg); + top: 0; + left: 0; + width: 100vw; + padding-bottom: 3rem; +} + +.header-mobile-container { + position: relative; + width: 100vw; + height: 100vh; + overflow: hidden; +} diff --git a/marketplace-ui/src/app/app.component.spec.ts b/marketplace-ui/src/app/app.component.spec.ts index a2bdd7a12..776d58bf3 100644 --- a/marketplace-ui/src/app/app.component.spec.ts +++ b/marketplace-ui/src/app/app.component.spec.ts @@ -7,6 +7,7 @@ import { RoutingQueryParamService } from './shared/services/routing.query.param. import { ActivatedRoute, RouterOutlet, NavigationStart, RouterModule, Router, NavigationError, Event } from '@angular/router'; import { of, Subject } from 'rxjs'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; import { ERROR_PAGE_PATH } from './shared/constants/common.constant'; describe('AppComponent', () => { @@ -15,6 +16,7 @@ describe('AppComponent', () => { let routingQueryParamService: jasmine.SpyObj; let activatedRoute: ActivatedRoute; let navigationStartSubject: Subject; + let appElement: HTMLElement; let router: Router; let routerEventsSubject: Subject; @@ -74,6 +76,10 @@ describe('AppComponent', () => { routingQueryParamService.getNavigationStartEvent.and.returnValue( navigationStartSubject.asObservable() ); + appElement = fixture.debugElement.query( + By.css('.app-container') + ).nativeElement; + activatedRoute = TestBed.inject(ActivatedRoute); router = TestBed.inject(Router); fixture.detectChanges(); @@ -118,6 +124,31 @@ describe('AppComponent', () => { ).not.toHaveBeenCalled(); }); + it('should hide scrollbar when burger menu is opened', () => { + component.isMobileMenuCollapsed = false; + fixture.detectChanges(); + + const headerElement = fixture.debugElement.query(By.css('.header-mobile')); + expect(headerElement).toBeTruthy(); + + expect(appElement.classList.contains('header-mobile-container')).toBeTrue(); + + const headerComputedStyle = window.getComputedStyle(appElement); + expect(headerComputedStyle.overflow).toBe('hidden'); + }); + + it('should reset header style when burger menu is closed', () => { + component.isMobileMenuCollapsed = true; + fixture.detectChanges(); + + const headerElement = fixture.debugElement.query(By.css('.header-mobile')); + expect(headerElement).toBeNull(); + + expect( + appElement.classList.contains('header-mobile-container') + ).toBeFalse(); + }); + it('should redirect to "/error-page" on NavigationError', () => { // Simulate a NavigationError event const navigationError = new NavigationError(1, '/a-trust/test-url', 'Error message'); diff --git a/marketplace-ui/src/app/app.component.ts b/marketplace-ui/src/app/app.component.ts index 3d5bced6f..c4016a8f8 100644 --- a/marketplace-ui/src/app/app.component.ts +++ b/marketplace-ui/src/app/app.component.ts @@ -1,16 +1,23 @@ -import { Component, inject } from '@angular/core'; -import { RouterOutlet, ActivatedRoute, Router, NavigationError, Event } from '@angular/router'; import { FooterComponent } from './shared/components/footer/footer.component'; import { HeaderComponent } from './shared/components/header/header.component'; import { LoadingService } from './core/services/loading/loading.service'; import { RoutingQueryParamService } from './shared/services/routing.query.param.service'; +import { CommonModule } from '@angular/common'; import { ERROR_PAGE_PATH } from './shared/constants/common.constant'; -import { LoadingSpinnerComponent } from './shared/components/loading-spinner/loading-spinner.component'; +import { Component, inject } from '@angular/core'; +import { + ActivatedRoute, + NavigationError, + Router, + RouterOutlet, + Event +} from '@angular/router'; +import { LoadingSpinnerComponent } from "./shared/components/loading-spinner/loading-spinner.component"; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, HeaderComponent, FooterComponent, LoadingSpinnerComponent], + imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule, LoadingSpinnerComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -18,8 +25,9 @@ export class AppComponent { loadingService = inject(LoadingService); routingQueryParamService = inject(RoutingQueryParamService); route = inject(ActivatedRoute); + isMobileMenuCollapsed = true; - constructor(private readonly router: Router) { } + constructor(private readonly router: Router) {} ngOnInit(): void { this.router.events.subscribe((event: Event) => { diff --git a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts index 822ba7918..19475e3d5 100644 --- a/marketplace-ui/src/app/core/interceptors/api.interceptor.ts +++ b/marketplace-ui/src/app/core/interceptors/api.interceptor.ts @@ -11,7 +11,7 @@ import { Router } from '@angular/router'; import { ERROR_CODES, ERROR_PAGE_PATH } from '../../shared/constants/common.constant'; export const REQUEST_BY = 'X-Requested-By'; -export const IVY = 'ivy'; +export const IVY = 'marketplace-website'; /** This is option for exclude loading api * @Example return httpClient.get('apiEndPoint', { context: new HttpContext().set(SkipLoading, true) }) diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html index dd414407c..687471ce6 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.html +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.html @@ -6,7 +6,8 @@ class="card-img-top rounded" width="70" height="70" - [ngSrc]="product | logo" + [ngSrc]="logoUrl" + (error)="onLogoError()" [alt]=" product.names | multilingualism: languageService.selectedLanguage() " diff --git a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts index 5de39f622..c268a4421 100644 --- a/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts +++ b/marketplace-ui/src/app/modules/product/product-card/product-card.component.ts @@ -4,14 +4,14 @@ import { TranslateModule } from '@ngx-translate/core'; import { LanguageService } from '../../../core/services/language/language.service'; import { ThemeService } from '../../../core/services/theme/theme.service'; import { Product } from '../../../shared/models/product.model'; -import { ProductLogoPipe } from '../../../shared/pipes/logo.pipe'; import { MultilingualismPipe } from '../../../shared/pipes/multilingualism.pipe'; import { ProductComponent } from '../product.component'; +import { DEFAULT_IMAGE_URL } from '../../../shared/constants/common.constant'; @Component({ selector: 'app-product-card', standalone: true, - imports: [CommonModule, ProductLogoPipe, MultilingualismPipe, TranslateModule, NgOptimizedImage], + imports: [CommonModule, MultilingualismPipe, TranslateModule, NgOptimizedImage], templateUrl: './product-card.component.html', styleUrl: './product-card.component.scss' }) @@ -21,4 +21,13 @@ export class ProductCardComponent { isShowInRESTClientEditor = inject(ProductComponent).isRESTClient(); @Input() product!: Product; + logoUrl = DEFAULT_IMAGE_URL; + + ngOnInit(): void { + this.logoUrl = this.product.logoUrl; + } + + onLogoError() { + this.logoUrl = DEFAULT_IMAGE_URL; + } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html index c7e919360..a32508c3d 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-information-tab/product-detail-information-tab.component.html @@ -50,13 +50,6 @@

{{ productDetail.type }}
-
- - {{ 'common.product.detail.information.value.industry' | translate }} - - {{ productDetail.industry }} -
-
{{ 'common.product.detail.information.value.tag' | translate }} diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html index 15e185a93..eac66489e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.html @@ -1,20 +1,12 @@

- - <!-- {{ productModuleContent.name }} --> -
- <dependency> -
-   <groupId>{{ productModuleContent.groupId }}</groupId> -
-   <artifactId>{{ - productModuleContent.artifactId - }}</artifactId> -
-   <version>{{ - selectedVersion.replaceAll('Version ', '') - }}</version> -
-   <type>{{ productModuleContent.type }}</type> -
- </dependency> -
\ No newline at end of file +
+  
+    <!-- {{ getProductName() }} -->
+    <dependency>
+      <groupId>{{ productDetail.productModuleContent.groupId }}</groupId>
+      <artifactId>{{productDetail.productModuleContent.artifactId}}</artifactId>
+      <version>{{ selectedVersion.replaceAll('Version ', '') }}</version>
+      <type>{{ productDetail.productModuleContent.type }}</type>
+    </dependency>
+  
+
\ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss index e69de29bb..0c04bfd6e 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.scss @@ -0,0 +1,3 @@ +pre { + border: 1px solid #c7d426; +} \ No newline at end of file diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts index 5fc90702b..6130b7de4 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.spec.ts @@ -15,7 +15,7 @@ describe('ProductDetailMavenContentComponent', () => { fixture = TestBed.createComponent(ProductDetailMavenContentComponent); component = fixture.componentInstance; - component.productModuleContent = MOCK_PRODUCT_DETAIL.productModuleContent; + component.productDetail = MOCK_PRODUCT_DETAIL; component.selectedVersion = '1.0.0'; fixture.detectChanges(); }); diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts index a645e68e9..bf944286c 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-maven-content/product-detail-maven-content.component.ts @@ -1,7 +1,8 @@ import { Component, inject, Input } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; -import { ProductModuleContent } from '../../../../shared/models/product-module-content.model'; import { LanguageService } from '../../../../core/services/language/language.service'; +import { Language } from '../../../../shared/enums/language.enum'; +import { ProductDetail } from '../../../../shared/models/product-detail.model'; @Component({ selector: 'app-product-detail-maven-content', @@ -12,9 +13,13 @@ import { LanguageService } from '../../../../core/services/language/language.ser }) export class ProductDetailMavenContentComponent { @Input() - productModuleContent!: ProductModuleContent; + productDetail!: ProductDetail; @Input() selectedVersion!: string; languageService = inject(LanguageService); + + getProductName() { + return this.productDetail.names[Language.EN]; + } } diff --git a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html index cf558e51b..ba5356303 100644 --- a/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html +++ b/marketplace-ui/src/app/modules/product/product-detail/product-detail-version-action/product-detail-version-action.component.html @@ -1,37 +1,39 @@ -@switch(actionType){ +@switch (actionType) { @case ('standard') { - } @case ('designerEnv') {
+ [selectedItem]="selectedVersion()" + buttonClass="form-select form-select-sm versions-selector__dropdown border__dropdown h-100 text-primary install-designer-dropdown" + ariaLabel=".form-select-sm example" class="flex-grow-1 col-8" + [metaDataJsonUrl]="metaDataJsonUrl()" + (itemSelected)="onSelectVersionInDesigner($event.value)"> } } @if (isDropDownDisplayed()) { -