Skip to content

Commit

Permalink
MARP-1687 Release Preview page
Browse files Browse the repository at this point in the history
  • Loading branch information
quanpham-axonivy committed Dec 24, 2024
1 parent e781fda commit dbcbdb6
Show file tree
Hide file tree
Showing 18 changed files with 826 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class DirectoryConstants {
public static final String DATA_DIR = "data";
public static final String WORK_DIR = "work";
public static final String CACHE_DIR = "market-cache";
public static final String PREVIEW_DIR = "marketplace-service/release-preview";
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class RequestMappingConstants {
public static final String GIT_HUB_LOGIN = "/github/login";
public static final String AUTH = "/auth";
public static final String BY_ID = "/{id}";
public static final String BY_FILE_NAME = "/preview/{imageName}";
public static final String BY_ID_AND_VERSION = "/{id}/{version}";
public static final String BEST_MATCH_BY_ID_AND_VERSION = "/{id}/{version}/bestmatch";
public static final String VERSIONS_BY_ID = "/{id}/versions";
Expand All @@ -33,4 +34,5 @@ public class RequestMappingConstants {
public static final String EXTERNAL_DOCUMENT = API + "/externaldocument";
public static final String PRODUCT_MARKETPLACE_DATA = API + "/product-marketplace-data";
public static final String SECURITY_MONITOR = API + "/security-monitor";
public static final String RELEASE_PREVIEW = API + "/release-preview";
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
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.RequestMappingConstants.*;
import static com.axonivy.market.constants.RequestParamConstants.ID;

@RestController
Expand Down Expand Up @@ -54,4 +53,19 @@ public ResponseEntity<byte[]> findImageById(
}
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
}

@GetMapping(BY_FILE_NAME)
public ResponseEntity<byte[]> findPreviewImageByName(
@PathVariable("imageName") String imageName) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
byte[] imageData = imageService.readPreviewImageByName(imageName);
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.axonivy.market.controller;

import com.axonivy.market.logging.Loggable;
import com.axonivy.market.model.DesignerInstallation;
import com.axonivy.market.service.ProductDesignerInstallationService;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -30,6 +31,7 @@ public ProductDesignerInstallationController(ProductDesignerInstallationService
this.productDesignerInstallationService = productDesignerInstallationService;
}

@Loggable
@GetMapping(DESIGNER_INSTALLATION_BY_ID)
@Operation(summary = "Get designer installation count by product id.",
description = "get designer installation count by product id")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.axonivy.market.controller;

import com.axonivy.market.model.ReleasePreview;
import com.axonivy.market.service.ReleasePreviewService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import static com.axonivy.market.constants.RequestMappingConstants.RELEASE_PREVIEW;

@Log4j2
@RestController
@RequestMapping(RELEASE_PREVIEW)
@Tag(name = "Release Preview Controller", description = "API to extract zip file and return README data.")
@AllArgsConstructor
public class ReleasePreviewController {

private final ReleasePreviewService previewService;

@PostMapping
@Operation()
public ResponseEntity<Object> extractZipFile(@RequestParam(value = "file") MultipartFile file) {
String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
ReleasePreview preview = previewService.extract(file, baseUrl);
if (preview == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return ResponseEntity.ok(preview);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.axonivy.market.model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Map;


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReleasePreview {

@Schema(description = "Product detail description content ",
example = "{ \"de\": \"E-Sign-Konnektor\", \"en\": \"E-sign connector\" }")
private Map<String, String> description;
@Schema(description = "Setup tab content", example = "{ \"de\": \"Setup\", \"en\": \"Setup\" ")
private Map<String, String> setup;
@Schema(description = "Demo tab content", example = "{ \"de\": \"Demo\", \"en\": \"Demo\" ")
private Map<String, String> demo;

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface ImageService {
Image mappingImageFromDownloadedFolder(String productId, Path imagePath);

byte[] readImage(String id);

byte[] readPreviewImageByName(String imageName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.axonivy.market.service;

import com.axonivy.market.model.ReleasePreview;
import org.springframework.web.multipart.MultipartFile;

public interface ReleasePreviewService {

ReleasePreview extract(MultipartFile file, String baseUrl);

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR;

@Service
@Log4j2
@AllArgsConstructor
Expand Down Expand Up @@ -109,4 +113,29 @@ public Image mappingImageFromDownloadedFolder(String productId, Path imagePath)
public byte[] readImage(String id) {
return imageRepository.findById(id).map(Image::getImageData).map(Binary::getData).orElse(null);
}

@Override
public byte[] readPreviewImageByName(String imageName) {
Path previewPath = Paths.get(PREVIEW_DIR);
if (!Files.exists(previewPath) || !Files.isDirectory(previewPath)) {
log.info("#readPreviewImageByName: Preview folder not found");
}
try {

Optional<Path> imagePath = Files.walk(previewPath)
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().equalsIgnoreCase(imageName))
.findFirst();
if (imagePath.isEmpty()) {
log.info("#readPreviewImageByName: Image with name {} is missing", imageName);
return null;
}
InputStream contentStream = MavenUtils.extractedContentStream(imagePath.get());
return IOUtils.toByteArray(contentStream);
} catch (IOException e) {
log.error("#readPreviewImageByName: Error when read preview image {}: {}", imageName, e.getMessage());
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.axonivy.market.service.impl;

import com.axonivy.market.constants.CommonConstants;
import com.axonivy.market.constants.ReadmeConstants;
import com.axonivy.market.model.ReadmeContentsModel;
import com.axonivy.market.model.ReleasePreview;
import com.axonivy.market.service.ReleasePreviewService;
import com.axonivy.market.util.ProductContentUtils;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR;
import static com.axonivy.market.util.ProductContentUtils.*;

@Log4j2
@Service
@AllArgsConstructor
public class ReleasePreviewServiceImpl implements ReleasePreviewService {

@Override
public ReleasePreview extract(MultipartFile file, String baseUrl) {
unzip(file);
return extractREADME(baseUrl);
}

private void unzip(MultipartFile file) {
try {
File extractDir = new File(PREVIEW_DIR);
if (extractDir.exists()) {
cleanDirectory(extractDir.toPath());
}

try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
File outFile = new File(extractDir, entry.getName());
if (entry.isDirectory()) {
outFile.mkdirs();
} else {
new File(outFile.getParent()).mkdirs();
try (FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buffer = new byte[1024];
int length;
while ((length = zipInputStream.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
}
}
zipInputStream.closeEntry();
}
}
} catch (IOException e) {
log.error("#unzip An exception occurred when unzip file {} - message {}", file.getName(), e.getMessage());
}
}

private ReleasePreview extractREADME(String baseUrl) {
Map<String, Map<String, String>> moduleContents = new HashMap<>();
try (Stream<Path> readmePathStream = Files.walk(Paths.get(PREVIEW_DIR))) {
List<Path> readmeFiles = readmePathStream.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().startsWith(ReadmeConstants.README_FILE_NAME))
.toList();
if (readmeFiles.isEmpty()) {
return null;
}
for (Path readmeFile : readmeFiles) {
String readmeContents = Files.readString(readmeFile);

if (ProductContentUtils.hasImageDirectives(readmeContents)) {
readmeContents = updateImagesWithDownloadUrl(PREVIEW_DIR, readmeContents, baseUrl);
}

ReadmeContentsModel readmeContentsModel = ProductContentUtils.getExtractedPartsOfReadme(readmeContents);

ProductContentUtils.mappingDescriptionSetupAndDemo(moduleContents, readmeFile.getFileName().toString(),
readmeContentsModel);
}
ReleasePreview preview = new ReleasePreview();
preview.setDescription(replaceEmptyContentsWithEnContent(moduleContents.get(DESCRIPTION)));
preview.setDemo(replaceEmptyContentsWithEnContent(moduleContents.get(DEMO)));
preview.setSetup(replaceEmptyContentsWithEnContent(moduleContents.get(SETUP)));
return preview;
} catch (IOException e) {
log.error("Cannot get README file's content from folder {}: {}",
com.axonivy.market.constants.DirectoryConstants.PREVIEW_DIR, e.getMessage());
}
return null;
}

public String updateImagesWithDownloadUrl(String unzippedFolderPath,
String readmeContents, String baseUrl) {
Map<String, String> imageUrls = new HashMap<>();
try (Stream<Path> imagePathStream = Files.walk(Paths.get(unzippedFolderPath))) {
List<Path> allImagePaths = imagePathStream.filter(Files::isRegularFile).filter(
path -> path.getFileName().toString().toLowerCase().matches(CommonConstants.IMAGE_EXTENSION)).toList();

allImagePaths.stream()
.filter(Objects::nonNull)
.forEach(imagePath -> {
String imageFileName = imagePath.getFileName().toString();
String imageIdFormat = String.format("%s/api/image/preview/%s", baseUrl, imageFileName);
imageUrls.put(imageFileName, imageIdFormat);
});
return ProductContentUtils.replaceImageDirWithImageCustomId(imageUrls, readmeContents);
} catch (Exception e) {
log.error("#updateImagesWithDownloadUrl: Error update image url: {}", e.getMessage());
return null;
}
}

private static void cleanDirectory(Path directory) {
try {
if (Files.exists(directory)) {
Files.walk(directory)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
Files.createDirectories(directory);
} catch (IOException e) {
log.error("#cleanDirectory: Error managing directory {} : {}", directory.toString(), e.getMessage());
}
}
}
7 changes: 6 additions & 1 deletion marketplace-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ErrorPageComponent } from './shared/components/error-page/error-page.co
import { RedirectPageComponent } from './shared/components/redirect-page/redirect-page.component';
import { ERROR_PAGE } from './shared/constants/common.constant';
import { SecurityMonitorComponent } from './modules/security-monitor/security-monitor.component';
import { ReleasePreviewComponent } from './modules/release-preview/release-preview.component';

export const routes: Routes = [
{
Expand All @@ -20,6 +21,10 @@ export const routes: Routes = [
path: 'security-monitor',
component: SecurityMonitorComponent
},
{
path: 'release-preview',
component: ReleasePreviewComponent
},
{
path: '',
loadChildren: () => import('./modules/home/home.routes').then(m => m.routes)
Expand All @@ -41,4 +46,4 @@ export const routes: Routes = [
path: 'auth/github/callback',
component: GithubCallbackComponent
}
];
];
Loading

0 comments on commit dbcbdb6

Please sign in to comment.