Skip to content

Commit

Permalink
Added the ManagerController Feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ramueSVA committed Nov 12, 2024
1 parent 5e102bf commit 7c2f299
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 358 deletions.
13 changes: 13 additions & 0 deletions src/main/java/org/highmed/numportal/domain/dto/QueryDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.highmed.numportal.domain.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
@Schema
public class QueryDto {

@NotNull private String aql;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;


@Data
@ConfigurationProperties(prefix = "feature")
public class FeatureProperties {

private boolean searchWithAql = false;

}
260 changes: 7 additions & 253 deletions src/main/java/org/highmed/numportal/service/ProjectService.java

Large diffs are not rendered by default.

238 changes: 238 additions & 0 deletions src/main/java/org/highmed/numportal/service/util/ExportUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package org.highmed.numportal.service.util;

import org.highmed.numportal.domain.model.Cohort;
import org.highmed.numportal.domain.model.ExportType;
import org.highmed.numportal.properties.ConsentProperties;
import org.highmed.numportal.properties.PrivacyProperties;
import org.highmed.numportal.service.CohortService;
import org.highmed.numportal.service.ProjectService;
import org.highmed.numportal.service.TemplateService;
import org.highmed.numportal.service.ehrbase.EhrBaseService;
import org.highmed.numportal.service.ehrbase.ResponseFilter;
import org.highmed.numportal.service.exception.PrivacyException;
import org.highmed.numportal.service.exception.ResourceNotFound;
import org.highmed.numportal.service.exception.SystemException;
import org.highmed.numportal.service.policy.EhrPolicy;
import org.highmed.numportal.service.policy.EuropeanConsentPolicy;
import org.highmed.numportal.service.policy.Policy;
import org.highmed.numportal.service.policy.ProjectPolicyService;
import org.highmed.numportal.service.policy.TemplatesPolicy;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.lang3.StringUtils;
import org.ehrbase.openehr.sdk.aql.dto.AqlQuery;
import org.ehrbase.openehr.sdk.response.dto.QueryResponseData;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.highmed.numportal.domain.templates.ExceptionsTemplate.AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL;
import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT;
import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_WHILE_CREATING_THE_CSV_FILE;
import static org.highmed.numportal.domain.templates.ExceptionsTemplate.RESULTS_WITHHELD_FOR_PRIVACY_REASONS;

@Slf4j
@AllArgsConstructor
public class ExportUtil {

private static final String CSV_FILE_PATTERN = "%s_%s.csv";
private static final String ZIP_FILE_ENDING = ".zip";
private static final String JSON_FILE_ENDING = ".json";
private static final String ZIP_MEDIA_TYPE = "application/zip";

private final CohortService cohortService;

private final TemplateService templateService;

private final EhrBaseService ehrBaseService;

private final ResponseFilter responseFilter;

private final PrivacyProperties privacyProperties;

private final ConsentProperties consentProperties;

private final ProjectPolicyService projectPolicyService;

private final ObjectMapper mapper;

public MultiValueMap<String, String> getExportHeaders(ExportType format, Long projectId) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
String fileEnding;
if (format == ExportType.json) {
fileEnding = JSON_FILE_ENDING;
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
} else {
fileEnding = ZIP_FILE_ENDING;
headers.add(HttpHeaders.CONTENT_TYPE, ZIP_MEDIA_TYPE);
}
headers.add(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + getExportFilenameBody(projectId) + fileEnding);
return headers;
}

public String getExportFilenameBody(Long projectId) {
return String.format(
"Project_%d_%s",
projectId,
LocalDateTime.now()
.truncatedTo(ChronoUnit.MINUTES)
.format(DateTimeFormatter.ISO_LOCAL_DATE))
.replace('-', '_');
}

public List<QueryResponseData> executeDefaultConfiguration(Long projectId, Cohort cohort, Map<String, String> templates) {

if (templates == null || templates.isEmpty()) {
return List.of();
}

Set<String> ehrIds = cohortService.executeCohort(cohort, false);

if (ehrIds.size() < privacyProperties.getMinHits()) {
log.warn(RESULTS_WITHHELD_FOR_PRIVACY_REASONS);
throw new PrivacyException(ProjectService.class, RESULTS_WITHHELD_FOR_PRIVACY_REASONS);
}

List<QueryResponseData> response = new LinkedList<>();

templates.forEach(
(templateId, v) ->
response.addAll(retrieveTemplateData(ehrIds, templateId, projectId, false)));
return responseFilter.filterResponse(response);
}

private List<QueryResponseData> retrieveTemplateData(
Set<String> ehrIds, String templateId, Long projectId, Boolean usedOutsideEu) {
try {
AqlQuery aql = templateService.createSelectCompositionQuery(templateId);

List<Policy> policies =
collectProjectPolicies(ehrIds, Map.of(templateId, templateId), usedOutsideEu);
projectPolicyService.apply(aql, policies);

List<QueryResponseData> response = ehrBaseService.executeRawQuery(aql, projectId);
response.forEach(data -> data.setName(templateId));
return response;

} catch (ResourceNotFound e) {
log.error("Could not retrieve data for template {} and project {}. Failed with message {} ", templateId, projectId, e.getMessage(), e);
log.error(e.getMessage(), e);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
QueryResponseData response = new QueryResponseData();
response.setName(templateId);
return List.of(response);
}

public List<Policy> collectProjectPolicies(
Set<String> ehrIds, Map<String, String> templates, boolean usedOutsideEu) {
List<Policy> policies = new LinkedList<>();
policies.add(EhrPolicy.builder().cohortEhrIds(ehrIds).build());
policies.add(TemplatesPolicy.builder().templatesMap(templates).build());

if (usedOutsideEu) {
policies.add(
EuropeanConsentPolicy.builder()
.oid(consentProperties.getAllowUsageOutsideEuOid())
.build());
}

return policies;
}

public StreamingResponseBody exportJson(List<QueryResponseData> response) {
String json;
try {
json = mapper.writeValueAsString(response);
} catch (JsonProcessingException e) {
throw new SystemException(ProjectService.class, AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL);
}
return outputStream -> {
outputStream.write(json.getBytes());
outputStream.flush();
outputStream.close();
};
}

public StreamingResponseBody exportCsv(List<QueryResponseData> response, Long projectId) {
return outputStream ->
streamResponseAsZip(response, getExportFilenameBody(projectId), outputStream);
}

public void streamResponseAsZip(
List<QueryResponseData> queryResponseDataList,
String filenameStart,
OutputStream outputStream) {

try (var zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {

var index = 0;
for (QueryResponseData queryResponseData : queryResponseDataList) {

String responseName = queryResponseData.getName();
if (StringUtils.isEmpty(responseName)) {
responseName = String.valueOf(index);
}
zipOutputStream.putNextEntry(
new ZipEntry(String.format(CSV_FILE_PATTERN, filenameStart, responseName)));
addResponseAsCsv(zipOutputStream, queryResponseData);
zipOutputStream.closeEntry();
index++;
}
} catch (IOException e) {
log.error("Error creating a zip file for data export.", e);
throw new SystemException(ProjectService.class, ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT,
String.format(ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT, e.getLocalizedMessage()));
}
}

private void addResponseAsCsv(ZipOutputStream zipOutputStream, QueryResponseData queryResponseData) {
List<String> paths = new ArrayList<>();

for (Map<String, String> column : queryResponseData.getColumns()) {
paths.add(column.get("path"));
}
CSVPrinter printer;
try {
printer =
CSVFormat.EXCEL.builder()
.setHeader(paths.toArray(new String[]{}))
.build()
.print(new OutputStreamWriter(zipOutputStream, StandardCharsets.UTF_8));

for (List<Object> row : queryResponseData.getRows()) {
printer.printRecord(row);
}
printer.flush();
} catch (IOException e) {
throw new SystemException(ProjectService.class, ERROR_WHILE_CREATING_THE_CSV_FILE,
String.format(ERROR_WHILE_CREATING_THE_CSV_FILE, e.getMessage()));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.highmed.numportal.web.controller;

import org.highmed.numportal.domain.dto.ManagerProjectDto;
import org.highmed.numportal.domain.dto.QueryDto;
import org.highmed.numportal.domain.model.ExportType;
import org.highmed.numportal.service.ManagerService;
import org.highmed.numportal.service.ehrbase.EhrBaseService;
import org.highmed.numportal.service.logger.ContextLog;
import org.highmed.numportal.service.util.ExportUtil;
import org.highmed.numportal.web.config.Role;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import org.ehrbase.openehr.sdk.response.dto.QueryResponseData;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.servlet.mvc.method.annotation.StreamingResponseBody;

@RestController
@AllArgsConstructor
@RequestMapping(value = "/query", produces = "application/json")
@SecurityRequirement(name = "security_auth")
@ConditionalOnProperty(value = "feature.search-with-aql", havingValue = "true")
public class ManagerController {

private final EhrBaseService ehrBaseService;
private final ManagerService managerService;
private final ExportUtil exportUtil;

@ContextLog(type = "Manager", description = "Execute AQL queries")
@PostMapping("execute")
@Operation(description = "Executes an AQL query")
@PreAuthorize(Role.MANAGER)
public ResponseEntity<QueryResponseData> executeManagerQuery(
@RequestBody @Valid QueryDto queryDto) {
return ResponseEntity.ok(
ehrBaseService.executePlainQuery(queryDto.getAql())
);
}

@PostMapping("/manager/execute")
@Operation(
description = "Executes the manager project aql in the cohort returning medical data matching the templates")
@PreAuthorize(Role.MANAGER)
public ResponseEntity<String> executeManagerProject(
@AuthenticationPrincipal @NotNull Jwt principal,
@RequestBody @Valid ManagerProjectDto managerProjectDto) {
return ResponseEntity.ok(
managerService.executeManagerProject(
managerProjectDto.getCohort(),
managerProjectDto.getTemplates(),
principal.getSubject()));
}

@PostMapping(value = "/manager/export")
@Operation(description = "Executes the cohort default configuration returns the result as a csv file attachment")
@PreAuthorize(Role.MANAGER)
public ResponseEntity<StreamingResponseBody> exportManagerResults(
@AuthenticationPrincipal @NotNull Jwt principal,
@RequestBody @Valid ManagerProjectDto managerProjectDto,
@RequestParam(required = false)
@Parameter(description = "A string defining the output format. Valid values are 'csv' and 'json'. Default is csv.")
ExportType format) {
StreamingResponseBody streamingResponseBody =
managerService.getManagerExportResponseBody(
managerProjectDto.getCohort(), managerProjectDto.getTemplates(), principal.getSubject(),
format);
MultiValueMap<String, String> headers = exportUtil.getExportHeaders(format, 0L);

return new ResponseEntity<>(streamingResponseBody, headers, HttpStatus.OK);
}
}
Loading

0 comments on commit 7c2f299

Please sign in to comment.