Skip to content

Commit

Permalink
[backend] Refactor report export with CSV (#945)
Browse files Browse the repository at this point in the history
* [backend] Refactor report export with CSA

* [backend] Fix deny dot star check

* ADM-739 refactor[backend]

* ADM-739 refactor[backend]

* ADM-739 add test[backend]

---------

Co-authored-by: Jianxun.Ma <jianxun.ma@rea-group.com>
Co-authored-by: guzhongren <guzhongren@live.cn>
  • Loading branch information
3 people authored Jan 16, 2024
1 parent 5fe0abb commit bcca032
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 139 deletions.
8 changes: 8 additions & 0 deletions backend/src/main/java/heartbeat/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import heartbeat.controller.board.dto.request.BoardType;
import heartbeat.controller.pipeline.dto.request.PipelineType;
import heartbeat.controller.report.dto.request.DataType;
import heartbeat.controller.report.dto.request.MetricType;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.source.SourceType;
import org.springframework.context.annotation.Configuration;
Expand All @@ -29,6 +30,13 @@ public DataType convert(String source) {
}
});

registry.addConverter(new Converter<String, MetricType>() {
@Override
public MetricType convert(String type) {
return MetricType.fromValue(type);
}
});

registry.addConverter(new Converter<String, ReportType>() {
@Override
public ReportType convert(String type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package heartbeat.controller.report;

import heartbeat.controller.report.dto.request.DataType;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.report.dto.request.MetricType;
import heartbeat.controller.report.dto.request.GenerateReportRequest;
import heartbeat.controller.report.dto.request.ExportCSVRequest;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.report.dto.response.CallbackResponse;
import heartbeat.controller.report.dto.response.ReportResponse;
import heartbeat.service.report.GenerateReporterService;
import heartbeat.service.report.ReportService;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -28,19 +29,24 @@
@RequestMapping("/reports")
@Validated
@Log4j2
public class GenerateReportController {
public class ReportController {

private final GenerateReporterService generateReporterService;

private final ReportService reportService;

@Value("${callback.interval}")
private Integer interval;

@GetMapping("/{dataType}/{filename}")
public InputStreamResource exportCSV(@PathVariable DataType dataType, @PathVariable String filename) {
log.info("Start to export CSV file, _dataType: {}, _timeStamp: {}", dataType, filename);
ExportCSVRequest request = new ExportCSVRequest(dataType.name().toLowerCase(), filename);
InputStreamResource result = generateReporterService.fetchCSVData(request);
log.info("Successfully get CSV file, _dataType: {}, _timeStamp: {}, _result: {}", dataType, filename, result);
@GetMapping("/{reportType}/{filename}")
public InputStreamResource exportCSV(
@Schema(type = "string", allowableValues = { "metric", "pipeline", "board" },
accessMode = Schema.AccessMode.READ_ONLY) @PathVariable() ReportType reportType,
@PathVariable String filename) {
log.info("Start to export CSV file_reportType: {}, _timeStamp: {}", reportType.getValue(), filename);
InputStreamResource result = reportService.exportCsv(reportType, Long.parseLong(filename));
log.info("Successfully get CSV file_reportType: {}, _timeStamp: {}, _result: {}", reportType.getValue(),
filename, result);
return result;
}

Expand All @@ -57,11 +63,13 @@ public ResponseEntity<ReportResponse> generateReport(@PathVariable String report
return ResponseEntity.status(HttpStatus.OK).body(reportResponse);
}

@PostMapping("{reportType}")
public ResponseEntity<CallbackResponse> generateReport(@PathVariable ReportType reportType,
@PostMapping("{metricType}")
public ResponseEntity<CallbackResponse> generateReport(
@Schema(type = "string", allowableValues = { "board", "dora" },
accessMode = Schema.AccessMode.READ_ONLY) @PathVariable MetricType metricType,
@RequestBody GenerateReportRequest request) {
CompletableFuture.runAsync(() -> {
switch (reportType) {
switch (metricType) {
case BOARD -> generateReporterService.generateBoardReport(request);
case DORA -> generateReporterService.generateDoraReport(request);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package heartbeat.controller.report.dto.request;

public enum MetricType {

BOARD, DORA;

public static MetricType fromValue(String type) {
return switch (type) {
case "board" -> BOARD;
case "dora" -> DORA;
default -> throw new IllegalArgumentException("ReportType not found!");
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@

public enum ReportType {

BOARD, DORA;
METRIC("metric"), PIPELINE("pipeline"), BOARD("board");

private String value;

ReportType(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static ReportType fromValue(String type) {
return switch (type) {
case "metric" -> METRIC;
case "pipeline" -> PIPELINE;
case "board" -> BOARD;
case "dora" -> DORA;
default -> throw new IllegalArgumentException("ReportType not found!");
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.google.gson.JsonElement;
import com.opencsv.CSVWriter;
import heartbeat.controller.board.dto.response.JiraCardDTO;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.report.dto.response.BoardCSVConfig;
import heartbeat.controller.report.dto.response.LeadTimeInfo;
import heartbeat.controller.report.dto.response.PipelineCSVInfo;
Expand Down Expand Up @@ -137,15 +138,14 @@ public void convertPipelineDataToCSV(List<PipelineCSVInfo> leadTimeData, String
}
}

public InputStreamResource getDataFromCSV(String dataType, long csvTimeStamp) {
return switch (dataType) {
case "metric" -> readStringFromCsvFile(
public InputStreamResource getDataFromCSV(ReportType reportType, long csvTimeStamp) {
return switch (reportType) {
case METRIC -> readStringFromCsvFile(
CSVFileNameEnum.METRIC.getValue() + FILENAME_SEPARATOR + csvTimeStamp + CSV_EXTENSION);
case "pipeline" -> readStringFromCsvFile(
case PIPELINE -> readStringFromCsvFile(
CSVFileNameEnum.PIPELINE.getValue() + FILENAME_SEPARATOR + csvTimeStamp + CSV_EXTENSION);
case "board" -> readStringFromCsvFile(
default -> readStringFromCsvFile(
CSVFileNameEnum.BOARD.getValue() + FILENAME_SEPARATOR + csvTimeStamp + CSV_EXTENSION);
default -> new InputStreamResource(new ByteArrayInputStream("".getBytes()));
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
import lombok.extern.log4j.Log4j2;
import lombok.val;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -803,12 +802,6 @@ private List<PipelineCSVInfo> generateCSVForPipelineWithCodebase(CodebaseSetting
return pipelineCSVInfos;
}

public InputStreamResource fetchCSVData(ExportCSVRequest request) {
long csvTimeStamp = Long.parseLong(request.getCsvTimeStamp());
validateExpire(csvTimeStamp);
return csvFileGenerator.getDataFromCSV(request.getDataType(), csvTimeStamp);
}

public boolean checkGenerateReportIsDone(String reportTimeStamp) {
if (validateExpire(System.currentTimeMillis(), Long.parseLong(reportTimeStamp))) {
throw new GenerateReportException("Failed to get report due to report time expires");
Expand All @@ -832,12 +825,6 @@ private ErrorInfo handleAsyncExceptionAndGetErrorInfo(BaseException exception) {
return null;
}

private void validateExpire(long csvTimeStamp) {
if (validateExpire(System.currentTimeMillis(), csvTimeStamp)) {
throw new NotFoundException("Failed to fetch CSV data due to CSV not found");
}
}

private void deleteOldCSV(long currentTimeStamp, File directory) {
File[] files = directory.listFiles();
if (!ObjectUtils.isEmpty(files)) {
Expand Down
28 changes: 28 additions & 0 deletions backend/src/main/java/heartbeat/service/report/ReportService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package heartbeat.service.report;

import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.InputStreamResource;
import org.springframework.stereotype.Service;

import static heartbeat.service.report.scheduler.DeleteExpireCSVScheduler.EXPORT_CSV_VALIDITY_TIME;

@Service
@RequiredArgsConstructor
public class ReportService {

private final CSVFileGenerator csvFileGenerator;

public InputStreamResource exportCsv(ReportType reportType, long csvTimestamp) {
if (isExpiredTimeStamp(csvTimestamp)) {
throw new NotFoundException("Failed to fetch CSV data due to CSV not found");
}
return csvFileGenerator.getDataFromCSV(reportType, csvTimestamp);
}

private boolean isExpiredTimeStamp(long timeStamp) {
return timeStamp < System.currentTimeMillis() - EXPORT_CSV_VALIDITY_TIME;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import heartbeat.controller.report.dto.request.ExportCSVRequest;
import heartbeat.controller.report.dto.request.GenerateReportRequest;
import heartbeat.controller.report.dto.request.ReportType;
import heartbeat.controller.report.dto.response.ReportResponse;
import heartbeat.exception.GenerateReportException;
import heartbeat.service.report.GenerateReporterService;
import heartbeat.handler.AsyncExceptionHandler;
import heartbeat.service.report.GenerateReporterService;
import heartbeat.service.report.ReportService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -22,20 +23,20 @@

import java.io.ByteArrayInputStream;
import java.io.File;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(GenerateReportController.class)
@WebMvcTest(ReportController.class)
@ExtendWith(SpringExtension.class)
@AutoConfigureJsonTesters
class GenerateReporterControllerTest {
class ReporterControllerTest {

private static final String REQUEST_FILE_PATH = "src/test/java/heartbeat/controller/report/request.json";

Expand All @@ -44,6 +45,9 @@ class GenerateReporterControllerTest {
@MockBean
private GenerateReporterService generateReporterService;

@MockBean
private ReportService reporterService;

@MockBean
private AsyncExceptionHandler asyncExceptionHandler;

Expand Down Expand Up @@ -114,16 +118,14 @@ void shouldReturnInternalServerErrorStatusWhenCheckGenerateReportThrowException(

@Test
void shouldReturnWhenExportCsv() throws Exception {
String dataType = "pipeline";
String csvTimeStamp = "1685010080107";
Long csvTimeStamp = 1685010080107L;
String expectedResponse = "csv data";

when(generateReporterService
.fetchCSVData(ExportCSVRequest.builder().dataType(dataType).csvTimeStamp(csvTimeStamp).build()))
when(reporterService.exportCsv(ReportType.PIPELINE, csvTimeStamp))
.thenReturn(new InputStreamResource(new ByteArrayInputStream(expectedResponse.getBytes())));

MockHttpServletResponse response = mockMvc
.perform(get("/reports/{dataType}/{csvTimeStamp}", dataType, csvTimeStamp))
.perform(get("/reports/{reportType}/{csvTimeStamp}", ReportType.PIPELINE.getValue(), csvTimeStamp))
.andExpect(status().isOk())
.andReturn()
.getResponse();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package heartbeat.controller.report.dto.request;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

class MetricTypeTest {

@Test
public void shouldConvertValueToType() {
MetricType boardType = MetricType.fromValue("board");
MetricType doraType = MetricType.fromValue("dora");

assertEquals(boardType, MetricType.BOARD);
assertEquals(doraType, MetricType.DORA);
}

@Test
public void shouldThrowExceptionWhenDateTypeNotSupported() {
assertThatThrownBy(() -> MetricType.fromValue("unknown")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("ReportType not found!");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.Assert.assertEquals;

class ReportTypeTest {

@Test
public void shouldConvertValueToType() {
ReportType boardType = ReportType.fromValue("board");
ReportType doraType = ReportType.fromValue("dora");
ReportType pipelineType = ReportType.fromValue("pipeline");
ReportType metricType = ReportType.fromValue("metric");

assertEquals(boardType, ReportType.BOARD);
assertEquals(doraType, ReportType.DORA);
assertEquals(pipelineType, ReportType.PIPELINE);
assertEquals(metricType, ReportType.METRIC);
}

@Test
Expand Down
Loading

0 comments on commit bcca032

Please sign in to comment.