Skip to content

Commit

Permalink
Moving ValidationCategorizer to the validation package.
Browse files Browse the repository at this point in the history
Adding unit test to ensure unique category ids are specified.
Updates categories to de-duplicate them, fixing the naming/guidance on at least one of them.
Add text/html output for the validation category results endpoint that produces an HTML file that can be downloaded and opened in a browser, showing the categories and validation result details.
Moving some logic for categorizing from the controller to `ValidationCategorizer`.
Adding validation-results.html as output in the folder-based submission so that they can be automatically generated and included in the submission for review.
  • Loading branch information
seanmcilvenna committed Mar 14, 2024
1 parent 256ff48 commit f58a740
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.lantanagroup.link.FhirHelper;
import com.lantanagroup.link.ValidationCategorizer;
import com.lantanagroup.link.config.api.ApiConfig;
import com.lantanagroup.link.db.SharedService;
import com.lantanagroup.link.db.TenantService;
import com.lantanagroup.link.db.mappers.ValidationResultMapper;
import com.lantanagroup.link.db.model.Report;
import com.lantanagroup.link.db.model.tenant.ValidationResult;
import com.lantanagroup.link.db.model.tenant.ValidationResultCategory;
import com.lantanagroup.link.model.*;
import com.lantanagroup.link.model.ValidationCategoriesAndResults;
import com.lantanagroup.link.model.ValidationCategory;
import com.lantanagroup.link.model.ValidationCategoryResponse;
import com.lantanagroup.link.time.StopwatchManager;
import com.lantanagroup.link.validation.RuleBasedValidationCategory;
import com.lantanagroup.link.validation.ValidationCategorizer;
import com.lantanagroup.link.validation.ValidationService;
import com.lantanagroup.link.validation.Validator;
import org.hl7.fhir.r4.model.Bundle;
Expand Down Expand Up @@ -126,17 +128,6 @@ public String getValidationSummary(@PathVariable String tenantId, @PathVariable
return this.getValidationSummary(outcome);
}

private static ValidationCategoryResponse buildUncategorizedCategory(int count) {
ValidationCategoryResponse response = new ValidationCategoryResponse();
response.setId("uncategorized");
response.setTitle("Uncategorized");
response.setSeverity(ValidationCategorySeverities.WARNING);
response.setAcceptable(false);
response.setGuidance("These issues need to be categorized.");
response.setCount(count);
return response;
}

/**
* Retrieves the validation categories for a report, for a custom set of categories
*
Expand Down Expand Up @@ -177,7 +168,7 @@ public List<ValidationCategoryResponse> getValidationForCustomCategories(@PathVa
.collect(Collectors.toList());

if (!uncategorizedResults.isEmpty()) {
responses.add(buildUncategorizedCategory(uncategorizedResults.size()));
responses.add(ValidationCategorizer.buildUncategorizedCategory(uncategorizedResults.size()));
}

return responses;
Expand Down Expand Up @@ -262,7 +253,7 @@ public List<ValidationCategoryResponse> getValidationCategories(@PathVariable St
.collect(Collectors.toList());

if (uncategorizedCount > 0) {
responses.add(buildUncategorizedCategory(uncategorizedCount));
responses.add(ValidationCategorizer.buildUncategorizedCategory(uncategorizedCount));
}

return responses;
Expand Down Expand Up @@ -404,35 +395,29 @@ public ValidationCategoriesAndResults getValidationCategoriesAndResults(@PathVar
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Report not found");
}

ValidationCategoriesAndResults categoriesAndResults = new ValidationCategoriesAndResults();
List<ValidationCategory> categories = ValidationCategorizer.loadAndRetrieveCategories();
List<ValidationResult> results = tenantService.getValidationResults(reportId);
List<ValidationResultCategory> resultCategories = tenantService.findValidationResultCategoriesByReport(reportId);
ValidationCategorizer categorizer = new ValidationCategorizer();
return categorizer.getValidationCategoriesAndResults(tenantService, report);
}

categoriesAndResults.setCategories(categories.stream()
.map(c -> {
ValidationCategoryResponse response = new ValidationCategoryResponse(c);
response.setCount(resultCategories.stream().filter(rc -> rc.getCategoryCode().equals(c.getId())).count());
return response;
})
.filter(c -> c.getCount() > 0)
.collect(Collectors.toList()));

categoriesAndResults.setResults(results.stream().map(r -> {
ValidationResultResponse response = new ValidationResultResponse();
response.setId(r.getId());
response.setCode(r.getCode());
response.setDetails(r.getDetails());
response.setSeverity(r.getSeverity());
response.setExpression(r.getExpression());
response.setPosition(r.getPosition());
response.setCategories(resultCategories.stream()
.filter(rc -> rc.getValidationResultId().equals(r.getId()))
.map(ValidationResultCategory::getCategoryCode)
.collect(Collectors.toList()));
return response;
}).collect(Collectors.toList()));

return categoriesAndResults;
@GetMapping(value = "/{tenantId}/{reportId}/category/result", produces = {"text/html"})
public String getValidationCategoriesAndResultsHtml(@PathVariable String tenantId, @PathVariable String reportId) {
try {
TenantService tenantService = TenantService.create(this.sharedService, tenantId);

if (tenantService == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tenant not found");
}

Report report = tenantService.getReport(reportId);

if (report == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Report not found");
}

ValidationCategorizer categorizer = new ValidationCategorizer();
return categorizer.getValidationCategoriesAndResultsHtml(tenantService, report);
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error loading validation categories from resources");
}
}
}
11 changes: 11 additions & 0 deletions api/src/main/resources/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,14 @@ paths:
required: true
schema:
type: string
- name: Accept
in: header
description: The desired response format
required: false
schema:
type: string
default: application/xml
enum: [ application/json, application/xml, text/html ]
responses:
'200':
description: successful response
Expand All @@ -1406,6 +1414,9 @@ paths:
'application/json':
schema:
$ref: '#/components/schemas/ValidationCategoriesAndResults'
'text/html':
schema:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'403':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.lantanagroup.link.model;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.r4.model.Device;
Expand All @@ -10,6 +12,8 @@
@Setter
public class ReportBase {
private String id;
@JacksonXmlProperty(localName = "measureId")
@JacksonXmlElementWrapper(localName = "measureIds")
private List<String> measureIds;
private String periodStart;
private String periodEnd;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.lantanagroup.link.db.model.Report;
import lombok.Getter;
import lombok.Setter;
import org.hl7.fhir.r4.model.Device;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -13,11 +15,32 @@
@Setter
@JacksonXmlRootElement(localName = "validation")
public class ValidationCategoriesAndResults {
@JacksonXmlProperty(localName = "report")
public Report report;

public Device device;

@JacksonXmlProperty(localName = "category")
@JacksonXmlElementWrapper(localName = "categories")
public List<ValidationCategoryResponse> categories = new ArrayList<>();

@JacksonXmlProperty(localName = "result")
@JacksonXmlElementWrapper(localName = "results")
public List<ValidationResultResponse> results = new ArrayList<>();

public ValidationCategoriesAndResults(Report report) {
this.report = new Report();
this.report.setId(report.getId());
this.report.setMeasureIds(report.getMeasureIds());
this.report.setPeriodStart(report.getPeriodStart());
this.report.setPeriodEnd(report.getPeriodEnd());
this.report.setStatus(report.getStatus());
this.report.setGeneratedTime(report.getGeneratedTime());
this.report.setSubmittedTime(report.getSubmittedTime());
//this.device = report.getDeviceInfo();
}

public Boolean isPreQualified() {
return this.categories.stream().noneMatch(c -> !c.getAcceptable() && c.getCount() > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.lantanagroup.link.config.sender.FileSystemSenderConfig;
import com.lantanagroup.link.db.TenantService;
import com.lantanagroup.link.db.model.Report;
import com.lantanagroup.link.validation.ValidationCategorizer;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
Expand All @@ -22,7 +24,6 @@
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import jakarta.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
Expand All @@ -31,7 +32,6 @@
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.stream.Collectors;

import static com.google.common.primitives.Bytes.concat;

Expand Down Expand Up @@ -154,7 +154,7 @@ private void saveToFile(Resource resource, String path) throws Exception {
logger.info("Saved submission bundle to file system: {}", path);
}

private void saveToFolder(Bundle bundle, String path) throws Exception {
private void saveToFolder(Bundle bundle, String path, TenantService tenantService, Report report) throws Exception {
File folder = new File(path);

if (!folder.exists() && !folder.mkdirs()) {
Expand Down Expand Up @@ -217,6 +217,13 @@ private void saveToFolder(Bundle bundle, String path) throws Exception {
if (otherResourcesBundle.hasEntry()) {
this.saveToFile(otherResourcesBundle, Paths.get(path, "other-resources.json").toString());
}

// Save validation results as HTML
logger.debug("Saving validation results as HTML");
String html = new ValidationCategorizer().getValidationCategoriesAndResultsHtml(tenantService, report);
if (StringUtils.isNotEmpty(html)) {
this.saveToFile(html.getBytes(StandardCharsets.UTF_8), Paths.get(path, "validation-report.html").toString());
}
}

@SuppressWarnings("unused")
Expand All @@ -237,7 +244,7 @@ public void send(TenantService tenantService, Bundle submissionBundle, Report re
if (this.config.getIsBundle()) {
this.saveToFile(submissionBundle, path);
} else {
this.saveToFolder(submissionBundle, path);
this.saveToFolder(submissionBundle, path, tenantService, report);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.lantanagroup.link;
package com.lantanagroup.link.validation;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lantanagroup.link.Helper;
import com.lantanagroup.link.db.TenantService;
import com.lantanagroup.link.db.model.Report;
import com.lantanagroup.link.db.model.tenant.ValidationResult;
import com.lantanagroup.link.db.model.tenant.ValidationResultCategory;
import com.lantanagroup.link.model.ValidationCategory;
import com.lantanagroup.link.model.ValidationCategorySeverities;
import com.lantanagroup.link.validation.RuleBasedValidationCategory;
import com.lantanagroup.link.model.*;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
Expand All @@ -17,11 +18,11 @@
import java.util.List;
import java.util.stream.Collectors;

@Getter
@Setter
public class ValidationCategorizer {
private static final Logger logger = LoggerFactory.getLogger(ValidationCategorizer.class.getName());

@Getter
@Setter
private List<RuleBasedValidationCategory> categories = new ArrayList<>();

public static List<ValidationCategory> loadAndRetrieveCategories() {
Expand Down Expand Up @@ -100,4 +101,73 @@ public List<ValidationResultCategory> categorize(List<ValidationResult> results)

return resultCategories;
}

public static ValidationCategoryResponse buildUncategorizedCategory(int count) {
ValidationCategoryResponse response = new ValidationCategoryResponse();
response.setId("uncategorized");
response.setTitle("Uncategorized");
response.setSeverity(ValidationCategorySeverities.WARNING);
response.setAcceptable(false);
response.setGuidance("These issues need to be categorized.");
response.setCount(count);
return response;
}

public ValidationCategoriesAndResults getValidationCategoriesAndResults(TenantService tenantService, Report report) {
ValidationCategoriesAndResults categoriesAndResults = new ValidationCategoriesAndResults(report);
List<ValidationCategory> categories = ValidationCategorizer.loadAndRetrieveCategories();
List<ValidationResult> results = tenantService.getValidationResults(report.getId());
List<ValidationResultCategory> resultCategories = tenantService.findValidationResultCategoriesByReport(report.getId());

categoriesAndResults.setCategories(categories.stream()
.map(c -> {
ValidationCategoryResponse response = new ValidationCategoryResponse(c);
response.setCount(resultCategories.stream().filter(rc -> rc.getCategoryCode().equals(c.getId())).count());
return response;
})
.filter(c -> c.getCount() > 0)
.collect(Collectors.toList()));

if (results.stream().anyMatch(r -> resultCategories.stream().noneMatch(rc -> rc.getValidationResultId().equals(r.getId())))) {
categoriesAndResults.getCategories().add(buildUncategorizedCategory(tenantService.countUncategorizedValidationResults(report.getId())));
}

categoriesAndResults.setResults(results.stream().map(r -> {
ValidationResultResponse resultResponse = new ValidationResultResponse();
resultResponse.setId(r.getId());
resultResponse.setCode(r.getCode());
resultResponse.setDetails(r.getDetails());
resultResponse.setSeverity(r.getSeverity());
resultResponse.setExpression(r.getExpression());
resultResponse.setPosition(r.getPosition());
resultResponse.setCategories(resultCategories.stream()
.filter(rc -> rc.getValidationResultId().equals(r.getId()))
.map(ValidationResultCategory::getCategoryCode)
.collect(Collectors.toList()));

if (resultResponse.getCategories().isEmpty()) {
resultResponse.getCategories().add("uncategorized");
}

return resultResponse;
}).collect(Collectors.toList()));

return categoriesAndResults;
}

public String getValidationCategoriesAndResultsHtml(TenantService tenantService, Report report) throws IOException {
ValidationCategoriesAndResults categoriesAndResults = this.getValidationCategoriesAndResults(tenantService, report);

if (categoriesAndResults.getResults() != null && categoriesAndResults.getResults().isEmpty()) {
return null;
}

ObjectMapper mapper = new ObjectMapper();

try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("validation-categories.html")) {
String json = mapper.writeValueAsString(categoriesAndResults);
String html = Helper.readInputStream(is);
return html.replace("var report = {};", "var report = " + json + ";");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.lantanagroup.link.Constants;
import com.lantanagroup.link.EventService;
import com.lantanagroup.link.Helper;
import com.lantanagroup.link.ValidationCategorizer;
import com.lantanagroup.link.db.SharedService;
import com.lantanagroup.link.db.TenantService;
import com.lantanagroup.link.db.mappers.ValidationResultMapper;
Expand Down

0 comments on commit f58a740

Please sign in to comment.