Skip to content
This repository has been archived by the owner on Sep 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #383 from admin-ch/feature/VACCINECER-2353_Automat…
Browse files Browse the repository at this point in the history
…isierung_für_Export_PowerBI

Feature/vaccinecer 2353 automatisierung für export power bi
  • Loading branch information
haraldloesing authored Oct 12, 2022
2 parents b5ac4f6 + 572e8a7 commit af664e5
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.admin.bag.covidcertificate.api;

import ch.admin.bag.covidcertificate.api.exception.AuthorizationError;
import ch.admin.bag.covidcertificate.api.exception.BiDataError;
import ch.admin.bag.covidcertificate.api.exception.ConvertCertificateError;
import ch.admin.bag.covidcertificate.api.exception.CreateCertificateError;
import ch.admin.bag.covidcertificate.api.exception.CsvError;
Expand Down Expand Up @@ -209,6 +210,9 @@ public class Constants {

public static final CreateCertificateError DATE_OF_BIRTH_AFTER_CERTIFICATE_DATE = new CreateCertificateError(1004, "Invalid dateOfBirth! Must be before the certificate date", HttpStatus.BAD_REQUEST);

public static final BiDataError DATES_NOT_VALID = new BiDataError(1101, "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16.", HttpStatus.BAD_REQUEST);
public static final BiDataError WRITING_CSV_RESULT_FAILED = new BiDataError(1102, "Writing the CSV result of the BI data export failed.", HttpStatus.INTERNAL_SERVER_ERROR);

public static final String VACCINATION_TOURIST_PRODUCT_CODE_SUFFIX = "_T";

public static final Integer EXPIRATION_PERIOD_24_MONTHS = 24;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ch.admin.bag.covidcertificate.api.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

import java.util.Objects;

@AllArgsConstructor
@Getter
public class BiDataError {
private final int errorCode;
private final String errorMessage;
private final HttpStatus httpStatus;

@Override
public String toString() {
return "{\"errorCode\":" + errorCode + "," +
"\"errorMessage\":\"" + errorMessage + "\"," +
"\"httpStatus\":\"" + httpStatus.name() + "\"}";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BiDataError that = (BiDataError) o;
return errorCode == that.errorCode && errorMessage.equals(that.errorMessage) && httpStatus == that.httpStatus;
}

@Override
public int hashCode() {
return Objects.hash(errorCode, errorMessage, httpStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch.admin.bag.covidcertificate.api.exception;

import lombok.Getter;
import org.springframework.core.NestedRuntimeException;

@Getter
public class BiDataException extends NestedRuntimeException {

private final BiDataError biDataError;

public BiDataException(BiDataError error) {
super(error.getErrorMessage());
this.biDataError = error;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ch.admin.bag.covidcertificate.api.response;

import ch.admin.bag.covidcertificate.domain.BiData;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.UUID;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class BiDataDto {
UUID id;
LocalDateTime timestamp;
String type;
String value;
String details;
String country;
String systemSource;
String apiGatewayId;
String inAppDeliveryCode;
Boolean fraud;
String keyIdentifier;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ch.admin.bag.covidcertificate.api.response;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;

@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class BiDataResponseDto {

@NonNull
private byte[] zip;
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,14 @@ public List<RoleData> getRoleMapping() {
}

/**
* Returns <code>true</code> for given function IF:
* <ul>
* <li>mandatory</li>
* is valid when either is <code>null</code> or the given role is part of the user's roles
* <li>one-of</li>
* is valid when either is <code>null</code> or one of the given roles is part of the user's roles
* </ul>
* <li>
* The given function is only permitted when both conditions are valid.
* Returns <code>true</code> for given function if the one-of setting contains the role needed
* for the function to be accessed. If one-of isn't configured false will be returned.
*
* @param roles the user's roles
* @param function the function to check
* @return <code>true</code> only if both mandatory and one-of are valid
* @return <code>true</code> for given function if the one-of setting contains the role needed
* for the function to be accessed. If one-of isn't configured false will be returned.
*/


public boolean isGranted(Set<String> roles, ServiceData.Function function) {
boolean isActive = function.isBetween(LocalDateTime.now());
if (!isActive) {
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/ch/admin/bag/covidcertificate/domain/BiData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ch.admin.bag.covidcertificate.domain;

import java.time.LocalDateTime;
import java.util.UUID;

public interface BiData {
UUID getId();
LocalDateTime getTimestamp();
String getType();
String getValue();
String getDetails();
String getCountry();
String getSystemSource();
String getApiGatewayId();
String getInAppDeliveryCode();
Boolean getFraud();
String getKeyIdentifier();
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package ch.admin.bag.covidcertificate.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

@Repository
public interface KpiDataRepository extends JpaRepository<KpiData, UUID> {
KpiData findByUvci(String uvci);

@Query(value = "select k.id as id" +
", k.timestamp as timestamp" +
", k.value as value" +
", k.details as details" +
", k.country as country" +
", k.systemSource as systemSource" +
", k.apiGatewayId as apiGatewayId" +
", k.inAppDeliveryCode as inAppDeliveryCode" +
", r.fraud as fraud" +
", k.keyIdentifier as keyIdentifier " +
"from KpiData k " +
"LEFT JOIN Revocation r on r.uvci = k.uvci " +
"WHERE k.timestamp BETWEEN :fromDateTime AND :toDateTime " +
"ORDER BY k.timestamp asc")
List<BiData> findAllByDateRange(@Param("fromDateTime") LocalDateTime fromDateTime, @Param("toDateTime") LocalDateTime toDateTime);
}
116 changes: 116 additions & 0 deletions src/main/java/ch/admin/bag/covidcertificate/service/BiDataService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package ch.admin.bag.covidcertificate.service;

import ch.admin.bag.covidcertificate.api.exception.BiDataException;
import ch.admin.bag.covidcertificate.api.response.BiDataDto;
import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto;
import ch.admin.bag.covidcertificate.domain.BiData;
import ch.admin.bag.covidcertificate.domain.KpiDataRepository;
import com.opencsv.CSVWriter;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static ch.admin.bag.covidcertificate.api.Constants.DATES_NOT_VALID;
import static ch.admin.bag.covidcertificate.api.Constants.WRITING_CSV_RESULT_FAILED;

@Service
@RequiredArgsConstructor
@Slf4j
public class BiDataService {

private final KpiDataRepository kpiDataRepository;

public BiDataResponseDto loadBiData(LocalDate fromDate, LocalDate toDate) throws BiDataException {

validateDateRange(fromDate, toDate);

LocalDateTime fromDateTime = fromDate.atTime(LocalTime.MIN);
LocalDateTime toDateTime = toDate.atTime(LocalTime.MAX);
List<BiData> biDataList = this.kpiDataRepository.findAllByDateRange(fromDateTime, toDateTime);
List<BiDataDto> biDataDtoList = biDataList.stream().map(this::convert).collect(Collectors.toList());

try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos)) {
byte[] csv = createCsvResponse(biDataDtoList);
var entry = new ZipEntry("kpi_prod_export_" +
fromDateTime.format(DateTimeFormatter.ISO_DATE) +
"-" +
toDateTime.format(DateTimeFormatter.ISO_DATE) +
".csv");
entry.setSize(csv.length);
zos.putNextEntry(entry);
zos.write(csv);
zos.closeEntry();
zos.close();
return new BiDataResponseDto(baos.toByteArray());
} catch (IOException ex) {
log.error("IOException creating CSV response", ex);
throw new BiDataException(WRITING_CSV_RESULT_FAILED);
}
}

private void validateDateRange(LocalDate fromDate, LocalDate toDate) {
if (fromDate == null || toDate == null) {
throw new BiDataException(DATES_NOT_VALID);
}
if (!fromDate.isBefore(toDate)) {
throw new BiDataException(DATES_NOT_VALID);
}
if (!(fromDate.plusMonths(1l).minusDays(1).isEqual(toDate) || fromDate.plusDays(6l).isEqual(toDate))) {
// time range is not one month or not one week
throw new BiDataException(DATES_NOT_VALID);
}
}

private byte[] createCsvResponse(List<BiDataDto> biDataDtos) throws IOException {
var returnFile = writeCsv(biDataDtos, Charset.defaultCharset());
return Files.readAllBytes(returnFile.toPath());
}

private File writeCsv(List<BiDataDto> biDataDtos, Charset charset) throws IOException {
var randomUUID = UUID.randomUUID();
var file = File.createTempFile("bi_data_" + randomUUID, ".csv");
try (var csvWriter = new CSVWriter(new FileWriter(file, charset))) {
StatefulBeanToCsv<BiDataDto> beanToCsv = new StatefulBeanToCsvBuilder<BiDataDto>(csvWriter)
.withSeparator(CSVWriter.DEFAULT_SEPARATOR)
.withApplyQuotesToAll(true)
.build();
beanToCsv.write(biDataDtos);
return file;
} catch (Exception e) {
Files.delete(file.toPath());
throw new BiDataException(WRITING_CSV_RESULT_FAILED);
}
}

private BiDataDto convert(BiData biData) {
return new BiDataDto(biData.getId(),
biData.getTimestamp(),
biData.getType(),
biData.getValue(),
biData.getDetails(),
biData.getCountry(),
biData.getSystemSource(),
biData.getApiGatewayId(),
biData.getInAppDeliveryCode(),
biData.getFraud(),
biData.getKeyIdentifier());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ch.admin.bag.covidcertificate.web.controller;

import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto;
import ch.admin.bag.covidcertificate.service.BiDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
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 java.time.LocalDate;

@RestController
@RequestMapping("/api/v1/bi-data/{fromDate}/{toDate}")
@RequiredArgsConstructor
@Slf4j
public class BiDataController {

private final BiDataService biDataService;

@GetMapping()
public BiDataResponseDto loadBiData(
@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate,
@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate) {

return biDataService.loadBiData(fromDate, toDate);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.admin.bag.covidcertificate.web.controller;

import ch.admin.bag.covidcertificate.api.exception.AuthorizationException;
import ch.admin.bag.covidcertificate.api.exception.BiDataException;
import ch.admin.bag.covidcertificate.api.exception.CacheNotFoundException;
import ch.admin.bag.covidcertificate.api.exception.ConvertCertificateException;
import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException;
Expand Down Expand Up @@ -63,6 +64,11 @@ protected ResponseEntity<Object> handleRevocationException(RevocationException e
return new ResponseEntity<>(ex.getError(), ex.getError().getHttpStatus());
}

@ExceptionHandler(value = {BiDataException.class})
protected ResponseEntity<Object> handleBiDataException(BiDataException ex) {
return new ResponseEntity<>(ex.getBiDataError(), ex.getBiDataError().getHttpStatus());
}

@ExceptionHandler(value = {AccessDeniedException.class, SecurityException.class})
protected ResponseEntity<Object> handleAccessDeniedException() {
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
Expand Down
Loading

0 comments on commit af664e5

Please sign in to comment.