diff --git a/api/src/main/java/com/lantanagroup/link/api/controller/ValidationController.java b/api/src/main/java/com/lantanagroup/link/api/controller/ValidationController.java index e32301772..a1e435519 100644 --- a/api/src/main/java/com/lantanagroup/link/api/controller/ValidationController.java +++ b/api/src/main/java/com/lantanagroup/link/api/controller/ValidationController.java @@ -2,7 +2,6 @@ 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; @@ -10,9 +9,12 @@ 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; @@ -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 * @@ -177,7 +168,7 @@ public List getValidationForCustomCategories(@PathVa .collect(Collectors.toList()); if (!uncategorizedResults.isEmpty()) { - responses.add(buildUncategorizedCategory(uncategorizedResults.size())); + responses.add(ValidationCategorizer.buildUncategorizedCategory(uncategorizedResults.size())); } return responses; @@ -262,7 +253,7 @@ public List getValidationCategories(@PathVariable St .collect(Collectors.toList()); if (uncategorizedCount > 0) { - responses.add(buildUncategorizedCategory(uncategorizedCount)); + responses.add(ValidationCategorizer.buildUncategorizedCategory(uncategorizedCount)); } return responses; @@ -404,35 +395,29 @@ public ValidationCategoriesAndResults getValidationCategoriesAndResults(@PathVar throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Report not found"); } - ValidationCategoriesAndResults categoriesAndResults = new ValidationCategoriesAndResults(); - List categories = ValidationCategorizer.loadAndRetrieveCategories(); - List results = tenantService.getValidationResults(reportId); - List 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"); + } } } diff --git a/api/src/main/resources/swagger.yml b/api/src/main/resources/swagger.yml index b6f991fb8..2c05a900f 100644 --- a/api/src/main/resources/swagger.yml +++ b/api/src/main/resources/swagger.yml @@ -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 @@ -1406,6 +1414,9 @@ paths: 'application/json': schema: $ref: '#/components/schemas/ValidationCategoriesAndResults' + 'text/html': + schema: + type: string '401': $ref: '#/components/responses/Unauthorized' '403': diff --git a/core/src/main/java/com/lantanagroup/link/model/ReportBase.java b/core/src/main/java/com/lantanagroup/link/model/ReportBase.java index 0c00578db..960531703 100644 --- a/core/src/main/java/com/lantanagroup/link/model/ReportBase.java +++ b/core/src/main/java/com/lantanagroup/link/model/ReportBase.java @@ -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; @@ -10,6 +12,8 @@ @Setter public class ReportBase { private String id; + @JacksonXmlProperty(localName = "measureId") + @JacksonXmlElementWrapper(localName = "measureIds") private List measureIds; private String periodStart; private String periodEnd; diff --git a/core/src/main/java/com/lantanagroup/link/model/ValidationCategoriesAndResults.java b/core/src/main/java/com/lantanagroup/link/model/ValidationCategoriesAndResults.java index bc9401767..b931c3031 100644 --- a/core/src/main/java/com/lantanagroup/link/model/ValidationCategoriesAndResults.java +++ b/core/src/main/java/com/lantanagroup/link/model/ValidationCategoriesAndResults.java @@ -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; @@ -13,6 +15,11 @@ @Setter @JacksonXmlRootElement(localName = "validation") public class ValidationCategoriesAndResults { + @JacksonXmlProperty(localName = "report") + public Report report; + + public Device device; + @JacksonXmlProperty(localName = "category") @JacksonXmlElementWrapper(localName = "categories") public List categories = new ArrayList<>(); @@ -20,4 +27,20 @@ public class ValidationCategoriesAndResults { @JacksonXmlProperty(localName = "result") @JacksonXmlElementWrapper(localName = "results") public List 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); + } } diff --git a/core/src/main/java/com/lantanagroup/link/sender/FileSystemSender.java b/core/src/main/java/com/lantanagroup/link/sender/FileSystemSender.java index f815d350b..6eb2afbc8 100644 --- a/core/src/main/java/com/lantanagroup/link/sender/FileSystemSender.java +++ b/core/src/main/java/com/lantanagroup/link/sender/FileSystemSender.java @@ -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; @@ -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; @@ -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; @@ -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()) { @@ -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") @@ -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); } } } diff --git a/core/src/main/java/com/lantanagroup/link/ValidationCategorizer.java b/core/src/main/java/com/lantanagroup/link/validation/ValidationCategorizer.java similarity index 52% rename from core/src/main/java/com/lantanagroup/link/ValidationCategorizer.java rename to core/src/main/java/com/lantanagroup/link/validation/ValidationCategorizer.java index 788f3f844..b402f5229 100644 --- a/core/src/main/java/com/lantanagroup/link/ValidationCategorizer.java +++ b/core/src/main/java/com/lantanagroup/link/validation/ValidationCategorizer.java @@ -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; @@ -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 categories = new ArrayList<>(); public static List loadAndRetrieveCategories() { @@ -100,4 +101,73 @@ public List categorize(List 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 categories = ValidationCategorizer.loadAndRetrieveCategories(); + List results = tenantService.getValidationResults(report.getId()); + List 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 + ";"); + } + } } diff --git a/core/src/main/java/com/lantanagroup/link/validation/ValidationService.java b/core/src/main/java/com/lantanagroup/link/validation/ValidationService.java index 7f254ab38..06888c7f2 100644 --- a/core/src/main/java/com/lantanagroup/link/validation/ValidationService.java +++ b/core/src/main/java/com/lantanagroup/link/validation/ValidationService.java @@ -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; diff --git a/core/src/main/resources/validation-categories.html b/core/src/main/resources/validation-categories.html new file mode 100644 index 000000000..31646bbf3 --- /dev/null +++ b/core/src/main/resources/validation-categories.html @@ -0,0 +1,237 @@ + + + + + Chart + + + + + + + + + + + + +
+
+
+
+
+
Report Information
+
+

Report ID:

+

+ Measures: +

    +

    +

    Reporting Period Start:

    +

    Reporting Period End:

    +
    +
    +
    +
    +
    +
    Validation Information
    +
    +

    Categories of Results:

    +

    Total Validation Results:

    +

    Uncategorized Results: 0

    +

    Pre-qualified?

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    Results for Category

    +

    + Guidance: +

    +

    + Severity: +

    + Acceptable: +

    +

    + + + + + + + + + + +
    SeverityMessageExpression
    +
    +
    + + + + \ No newline at end of file diff --git a/core/src/main/resources/validation-categories.json b/core/src/main/resources/validation-categories.json index 8525489f1..d1bb7d05e 100644 --- a/core/src/main/resources/validation-categories.json +++ b/core/src/main/resources/validation-categories.json @@ -29,9 +29,13 @@ { "field": "DETAILS_TEXT", "regex": "^Unknown Code System '.*'$" + }, + { + "field": "DETAILS_TEXT", + "regex": "A code with no system .* A system should be provided" } ], - "andOperator": true + "andOperator": false } ] }, @@ -76,11 +80,7 @@ }, { "field": "DETAILS_TEXT", - "regex": "^The value provided \\(.*\\) is not in the value set '.*' \\(.*\\), and a code is required from this value set" - }, - { - "field": "DETAILS_TEXT", - "regex": "^The code provided \\(.+?\\) is not in the value set '.+?' \\(.+?\\), and a code from this value set is required" + "regex": "The (code|value) provided \\(.*\\) is not in the value set '.*' \\(http:\\/\\/.*\\), and a code .*is required" } ], "andOperator": false @@ -468,9 +468,19 @@ "field": "DETAILS_TEXT", "regex": "'http:\\/\\/lantanagroup\\.com\\/fhir\\/nhsn-measures.*'", "inverse": false + }, + { + "field": "DETAILS_TEXT", + "regex": "The value provided \\('(?:[A-Za-z]+|)'\\) is not in the value set 'USPS Two Letter Alphabetic Codes' \\(http:\\/\\/hl7\\.org\\/fhir\\/us\\/core\\/ValueSet\\/us-core-usps-state\\|(?:\\d+\\.\\d+\\.\\d+)?\\), and a code should come from this value set unless it has no suitable code \\(note that the validator cannot judge what is suitable\\)", + "inverse": false + }, + { + "field": "DETAILS_TEXT", + "regex": "Could not confirm that the codes provided are in the value set \\'.*\\' \\(http\\:\\/\\/terminology\\.hl7\\.org\\/ValueSet\\/.*\\)\\, and a code should come from this value set unless it has no suitable code .*", + "inverse": false } ], - "andOperator": true + "andOperator": false } ] }, @@ -566,11 +576,11 @@ ] }, { - "id": "Invalid_whitespace_(non-identifier)", - "title": "Invalid whitespace (non-identifier).", + "id": "Unresolved_url", + "title": "Unresolved URL", "severity": "WARNING", - "acceptable": true, - "guidance": "Source systems SHOULD remove any leading or trailing whitespace in text elements before sending.", + "acceptable": true, + "guidance": "The URL in the validation details is not resolvable. Link may not have the profiles/resources loaded that are represented by the canonical URL.", "ruleSets": [ { "rules": [ @@ -590,11 +600,11 @@ ] }, { - "id": "Invalid_whitespace_non_identifier", - "title": "Invalid whitespace non identifier", - "severity": "WARNING", - "acceptable": true, - "guidance": "Source systems SHOULD remove any leading or trailing whitespace in text elements before sending.", + "id": "Invalid_code_system", + "title": "Invalid code system.", + "severity": "ERROR", + "acceptable": true, + "guidance": "URI must be valid and the System should identify a valid coding system.", "ruleSets": [ { "rules": [ @@ -614,11 +624,11 @@ ] }, { - "id": "Invalid_code_system", - "title": "Invalid code system.", - "severity": "ERROR", - "acceptable": true, - "guidance": "URI must be valid and the System should identify a valid coding system.", + "id": "Invalid_whitespace_(non-identifier)", + "title": "Invalid whitespace (non-identifier).", + "severity": "WARNING", + "acceptable": true, + "guidance": "Source systems SHOULD remove any leading or trailing whitespace in text elements before sending.", "ruleSets": [ { "rules": [ @@ -636,5 +646,24 @@ "andOperator": false } ] + }, + { + "id": "Invalid_dateTime_format", + "title": "Invalid dateTime format.", + "severity": "ERROR", + "acceptable": false, + "guidance": "A date-time format must include the timezone offset for precision. Captured as +/-zzzz.", + "ruleSets": [ + { + "rules": [ + { + "field": "DETAILS_TEXT", + "regex": "date has a time, it must have a timezone.*", + "inverse": false + } + ], + "andOperator": false + } + ] } ] diff --git a/core/src/test/java/com/lantanagroup/link/validation/CategorizerTests.java b/core/src/test/java/com/lantanagroup/link/validation/CategorizerTests.java index bf86f5d0f..2bc9b09e9 100644 --- a/core/src/test/java/com/lantanagroup/link/validation/CategorizerTests.java +++ b/core/src/test/java/com/lantanagroup/link/validation/CategorizerTests.java @@ -1,14 +1,11 @@ package com.lantanagroup.link.validation; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.lantanagroup.link.ValidationCategorizer; 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 org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import java.io.IOException; @@ -72,85 +69,19 @@ public void categoryTest1() { Assert.assertEquals(1, categorizedResults.size()); } + /** + * Checks that the validation categories defined in the core/resources are unique, + * and don't duplicate ids. + */ @Test - @Ignore - public void serializeCategories() throws JsonProcessingException { + public void uniqueValidationCategories() { ValidationCategorizer categorizer = new ValidationCategorizer(); - ValidationCategorizer.loadAndRetrieveCategories().clear(); - - categorizer.addCategory("Can't validate code", ValidationCategorySeverities.WARNING, true, "There is an issue with the way the CodeSystem is set up on the terminology server. The full code set for the system does not appear to be on the server. The terminology server should be updated.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^The system .* http:\\/\\/hl7\\.org/fhir\\/sid\\/icd-9-cm was found but did not contain enough information to properly validate the code \\(mode = fragment\\) \\(from Tx-Server\\)"); - categorizer.addCategory("Unresolved Epic Code System URI", ValidationCategorySeverities.INFORMATION, true, "This is an Epic proprietary Code System and is only a concern if there is not another coding that provides a standard recognized coding Code System (which is handled under another logged issue)") - .addRuleSet(false) - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Code System URI ''urn:oid:1\\.2\\.840\\.114350.*'' is unknown so the code cannot be validated") - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Code System URI ''http://.*epic\\.com/.*'' is unknown so the code cannot be validated"); - categorizer.addCategory("Unresolved Medispan Code System URI", ValidationCategorySeverities.INFORMATION, true, "This is an Medispan proprietary Code System and is only a concern if there is not another coding that provides a standard recognized coding Code System (which is handled under another logged issue)") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Code System URI ''urn:oid:2\\.16\\.840\\.1\\.113883\\.6\\.68'' is unknown so the code cannot be validated"); - categorizer.addCategory("Unresolved and Unrecognized Code System URI", ValidationCategorySeverities.INFORMATION, true, "This is an unrecognized Code System and is only a concern if there is not another coding that provides a standard recognized coding Code System (which is handled under another logged issue)") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Code System URI ''.*'' is unknown so the code cannot be validated") - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "%urn:oid:1.2.840.114350%", true) - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "%http://%epic.com%", true) - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "%urn:oid:2.16.840.1.113883.6.68%", true); - categorizer.addCategory("Unknown Extension", ValidationCategorySeverities.INFORMATION, true, "Systems are allowed to include extensions (additional data). Extensions that do not modify the meaning of the data (modifierExtensions) can be safely ignored. This is not a modifierExtension.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^The extension .* is unknown, and not allowed here$"); - categorizer.addCategory("Does not match a slice", ValidationCategorySeverities.INFORMATION, true, "This could indicate an underlying issue in the resource (the resource is not validating). FHIR SME may need to review.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^This element does not match any known slice defined in the profile"); - categorizer.addCategory("Unable to match profile", ValidationCategorySeverities.ERROR, false, "This could indicate an underlying issue in the resource (the resource is not validating). FHIR SME may need to review.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Unable to find a match for profile .* among choices:"); - categorizer.addCategory("Does not match preferred ValueSet", ValidationCategorySeverities.INFORMATION, true, "This could be indicative of a problem if the data element is part of the measure and would not enable the resource to be included in the measure calculation appropriately.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^None of the codings provided are in the value set ''.*'' .*, and a coding is recommended to come from this value set"); - categorizer.addCategory("Does not match extensible ValueSet", ValidationCategorySeverities.WARNING, false, "This could be indicative of a problem if the data element is part of the measure and would not enable the resource to be included in the measure calculation appropriately.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^None of the codings provided are in the value set ''.*'' .*, and a coding should come from this value set unless it has no suitable code \\(note that the validator cannot judge what is suitable\\)"); - categorizer.addCategory("Possible matching profile", ValidationCategorySeverities.INFORMATION, true, "This could indicate an underlying issue in the resource (the resource is not validating). FHIR SME may need to review.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Details for .* matching against profile"); - categorizer.addCategory("Identifier.type code not provided from IdentifyType VS", ValidationCategorySeverities.WARNING, true, "This is only for business identifiers. Not important, unless a business identifier, such as MRN is required to be identified.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^No code provided, and a code should be provided from the value set ''.*'' \\(http:\\/\\/hl7\\.org\\/fhir\\/ValueSet\\/identifier-type\\|4\\.0\\.1\\)"); - categorizer.addCategory("MedicationRequest.requestor does not have a proper reference", ValidationCategorySeverities.WARNING, true, "No identity or reference for requester (Provider). Safe to ignore when ordering provider is not needed.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "A Reference without an actual reference or identifier should have a display") - .addRule(ValidationCategoryRule.Field.EXPRESSION, "Bundle.entry[\\[[0-9]+\\]\\.resource\\/.*\\*\\/\\.requester"); - categorizer.addCategory("Identifier value starts with whitespace", ValidationCategorySeverities.WARNING, true, "This is a business identifier with whitespace at the front or back. Not important if business identifiers are not used. May want to have the whitespace trimmed.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^value should not start or finish with whitespace ''.*''") - .addRule(ValidationCategoryRule.Field.EXPRESSION, "resource.*\\.identifier\\[[0-9]+\\]\\.value"); - categorizer.addCategory("No measure score allowed with cohort", ValidationCategorySeverities.ERROR, false, "The MeasureReport violates a business rule regarding MeasureScore. This issue needs to be resolved in the source data, the validator, or explicitely deemed an invalid error.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^No measureScore when the scoring of the message is ''''cohort''''"); - categorizer.addCategory("Invalid whitespace (non-identifier)", ValidationCategorySeverities.WARNING, true, "This is a non-identifier string element with whitespace at the front or back. Not generally important. May want to have the whitespace trimmed.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^value should not start or finish with whitespace ''.*''") - .addRule(ValidationCategoryRule.Field.EXPRESSION, "resource.*\\.\\w+\\[[0-9]+\\]\\.value", true); - categorizer.addCategory("Minimum slice occurrence not met", ValidationCategorySeverities.ERROR, true, "This is likely a secondary issue. If the resource can't validate then it can't be counted as meeting the slicing requirements. Likely addressing the underlying issue will make this issue go away. ") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "^Bundle.entry:.*: minimum required = .*, but only found .* \\(.*\\)"); - categorizer.addCategory("Link Error using old URL", ValidationCategorySeverities.WARNING, false, "This is an issue with NHSNLink (using an old url) and should be reported in Jira.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "'http:\\/\\/lantanagroup\\.com\\/fhir\\/nhsn-measures.*'"); - categorizer.addCategory("No codes from an extensible binding ValueSet", ValidationCategorySeverities.WARNING, false, "The code provided is not part of the extensible ValueSet, which if it is a concept that is part of the measure, is a problem that needs to be resolved.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "None of the codings provided are in the value set ''.*'' \\(.*\\), and a coding should come from this value set unless it has no suitable code \\(note that the validator cannot judge what is suitable\\)"); - categorizer.addCategory("No code provided", ValidationCategorySeverities.WARNING, false, "No code was provided, which if it is a concept that is part of the measure, is a problem that needs to be resolved.") - .addRuleSet() - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "No code provided, and a code should be provided from the value set ''.*'' \\(.*\\)"); - categorizer.addCategory("Unable to validate measure (Measure not found)", ValidationCategorySeverities.WARNING, false, "This appears to be an issue in the validation process and should be resolved as it may be hiding other issues.") - .addRuleSet(false) - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "Canonical URL ''.*'' does not resolve") - .addRule(ValidationCategoryRule.Field.DETAILS_TEXT, "The Measure ''.*'' could not be resolved, so no validation can be performed against the Measure"); - - categorizer.getCategories().forEach(c -> c.setId(c.getTitle().replaceAll("[^a-zA-Z0-9]", "_"))); - - System.out.println("Validation Categories:"); - System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(categorizer.getCategories())); - System.out.println(String.join("\n", categorizer.getCategories().stream().map(ValidationCategory::getId).toArray(String[]::new))); + categorizer.loadFromResources(); + categorizer.getCategories().stream() + .map(ValidationCategory::getId) + .collect(Collectors.groupingBy(id -> id, Collectors.counting())) + .forEach((id, count) -> { + Assert.assertEquals("Duplicate id: " + id, 1, count.longValue()); + }); } }