diff --git a/.gitignore b/.gitignore index 4d958ca72..76e4acb0a 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,4 @@ core.* javacore.* jitdump.* Snap.* +tooling/src/test/resources/casereporting/tes/output400/* diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/AdditionalContextGrouperEntry.java b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/AdditionalContextGrouperEntry.java new file mode 100644 index 000000000..c4bf8670c --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/AdditionalContextGrouperEntry.java @@ -0,0 +1,73 @@ +package org.opencds.cqf.tooling.casereporting.tes; + +public class AdditionalContextGrouperEntry extends TESGrouperEntry { + private String additionalContextGrouperUrl; + + public String getAdditionalContextGrouperUrl() { + return additionalContextGrouperUrl; + } + + private String additionalContextGrouperTitle; + + public String getAdditionalContextGrouperTitle() { + return additionalContextGrouperTitle; + } + + private String additionalContextGrouperValueSetUrl; + + public String getAdditionalContextGrouperValueSetUrl() { + return additionalContextGrouperValueSetUrl; + } + + private String additionalContextGrouperValueSetTitle; + + public String getAdditionalContextGrouperValueSetTitle() { + return additionalContextGrouperValueSetTitle; + } + + private String additionalContextGrouperValueSetCodeSystem; + + public String getAdditionalContextGrouperValueSetCodeSystem() { + return additionalContextGrouperValueSetCodeSystem; + } + + private String additionalContextGrouperCode; + + public String getAdditionalContextGrouperCode() { + return additionalContextGrouperCode; + } + + private String additionalContextGrouperCodeDisplay; + + public String getAdditionalContextGrouperCodeDisplay() { + return additionalContextGrouperCodeDisplay; + } + + private String additionalContextGrouperCodeSystemUrl; + + public String getAdditionalContextGrouperCodeSystemUrl() { + return additionalContextGrouperCodeSystemUrl; + } + + public AdditionalContextGrouperEntry( + String conditionGrouperUrl, + String conditionGrouperTitle, + String acGrouperCanonicalUrl, + String additionalContextGrouperTitle, + String additionalContextGrouperValueSetUrl, + String additionalContextGrouperValueSetTitle, + String additionalContextGrouperValueSetCodeSystem, + String additionalContextGrouperCode, + String additionalContextGrouperCodeDisplay, + String additionalContextGrouperCodeSystemUrl) { + super(conditionGrouperUrl, conditionGrouperTitle); + this.additionalContextGrouperUrl = acGrouperCanonicalUrl; + this.additionalContextGrouperTitle = additionalContextGrouperTitle; + this.additionalContextGrouperValueSetUrl = additionalContextGrouperValueSetUrl; + this.additionalContextGrouperValueSetTitle = additionalContextGrouperValueSetTitle; + this.additionalContextGrouperValueSetCodeSystem = additionalContextGrouperValueSetCodeSystem; + this.additionalContextGrouperCode = additionalContextGrouperCode; + this.additionalContextGrouperCodeDisplay = additionalContextGrouperCodeDisplay; + this.additionalContextGrouperCodeSystemUrl = additionalContextGrouperCodeSystemUrl; + } + } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/ConditionGrouperEntry.java b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/ConditionGrouperEntry.java new file mode 100644 index 000000000..49c9eca7c --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/ConditionGrouperEntry.java @@ -0,0 +1,34 @@ +package org.opencds.cqf.tooling.casereporting.tes; + +public class ConditionGrouperEntry extends TESGrouperEntry { + + private String reportingSpecificationGrouperTitle; + + public String getReportingSpecificationGrouperTitle() { + return reportingSpecificationGrouperTitle; + } + + private String reportingSpecificationConditionCode; + + public String getReportingSpecificationConditionCode() { + return reportingSpecificationConditionCode; + } + + private String reportingSpecificationConditionDescription; + + public String getReportingSpecificationConditionDescription() { + return reportingSpecificationConditionDescription; + } + + public ConditionGrouperEntry( + String conditionGrouperUrl, + String conditionGrouperTitle, + String reportingSpecificationGrouperTitle, + String reportingSpecificationConditionCode, + String reportingSpecificationConditionDescription) { + super(conditionGrouperUrl, conditionGrouperTitle); + this.reportingSpecificationGrouperTitle = reportingSpecificationGrouperTitle; + this.reportingSpecificationConditionCode = reportingSpecificationConditionCode; + this.reportingSpecificationConditionDescription = reportingSpecificationConditionDescription; + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/CurationCoverageLevelEntry.java b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/CurationCoverageLevelEntry.java new file mode 100644 index 000000000..d7bc93a2d --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/CurationCoverageLevelEntry.java @@ -0,0 +1,47 @@ +package org.opencds.cqf.tooling.casereporting.tes; + +public class CurationCoverageLevelEntry { + + private String level; + + public String getLevel() { + return level; + } + + private String levelReason; + + public String getLevelReason() { + return levelReason; + } + + private String author; + + public String getAuthor() { + return author; + } + + private String dateTime; + + public String getDateTime() { + return dateTime; + } + + private String note; + + public String getNote() { + return note; + } + + public CurationCoverageLevelEntry( + String level, + String levelReason, + String author, + String dateTime, + String note) { + this.level = level; + this.levelReason = levelReason; + this.author = author; + this.dateTime = dateTime; + this.note = note; + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESGrouperEntry.java b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESGrouperEntry.java new file mode 100644 index 000000000..2cd670fa0 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESGrouperEntry.java @@ -0,0 +1,20 @@ +package org.opencds.cqf.tooling.casereporting.tes; + +public class TESGrouperEntry { + private String conditionGrouperUrl; + + public String getConditionGrouperUrl() { + return conditionGrouperUrl; + } + + private String conditionGrouperTitle; + + public String getConditionGrouperTitle() { + return conditionGrouperTitle; + } + + public TESGrouperEntry(String conditionGrouperUrl, String conditionGrouperTitle) { + this.conditionGrouperUrl = conditionGrouperUrl; + this.conditionGrouperTitle = conditionGrouperTitle; + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESPackageGenerator.java b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESPackageGenerator.java index 0b46f8e2c..1f081ced0 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESPackageGenerator.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/casereporting/tes/TESPackageGenerator.java @@ -21,6 +21,9 @@ import org.opencds.cqf.tooling.utilities.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; public class TESPackageGenerator extends Operation { private static final Logger logger = LoggerFactory.getLogger(TESPackageGenerator.class); @@ -33,6 +36,11 @@ public class TESPackageGenerator extends Operation { private String outputFileName; private Set outputFileEncodings; + Map conditionGrouperCurationCoverageLevelByUrlMap = new HashMap<>(); + Map conditionGrouperCurationCoverageLevelByTitleMap = new HashMap<>(); + Map additionalContextGrouperCurationCoverageLevelByUrlMap = new HashMap<>(); + Map additionalContextGrouperCurationCoverageLevelByTitleMap = new HashMap<>(); + public Set getOutputFileEncodings() { if (outputFileEncodings == null) { outputFileEncodings = new HashSet<>(); @@ -166,13 +174,9 @@ public Bundle generatePackage(TESPackageGenerateParameters params) { for (ValueSet reportingSpecificationGrouper : reportingSpecificationGroupers) { conditionGroupingEntries.stream() .filter(cge -> ("ReportingSpecificationGrouper" - + cge.getReportingSpecificationConditionCode() - .replace('\u00A0', ' ') - .trim()) - .equalsIgnoreCase(reportingSpecificationGrouper - .getName() - .replace('\u00A0', ' ') - .trim())) + + normalize(cge.getReportingSpecificationConditionCode())) + .equalsIgnoreCase(normalize(reportingSpecificationGrouper + .getName()))) .collect(Collectors.toList()) .stream() .findFirst() @@ -275,7 +279,7 @@ public Bundle generatePackage(TESPackageGenerateParameters params) { } } - writeGeneratedGrouperUrlsToWorkbook(additionalContextGroupers); + writeGeneratedGrouperUrlsToWorkbook(conditionGroupers, additionalContextGroupers); List resourcesToBundle = new ArrayList<>(); resourcesToBundle.add(manifest); @@ -319,6 +323,7 @@ private List loadConditionGroupingDefinitions(String path List conditionGroupingEntries = new ArrayList<>(); try { Workbook workbook = SpreadsheetHelper.getWorkbook(pathToTESGrouperWorkbook); + loadCurationCoverage(workbook); conditionGroupingEntries = processConditionGroupingsSheet(workbook); return conditionGroupingEntries; } catch (Exception e) { @@ -379,14 +384,6 @@ private List processConditionGroupingsSheet(Workbook work private List generateConditionGroupers(List conditionGroupingEntries) { List conditionGroupers = new ArrayList<>(); - List extensions = new ArrayList<>(); - extensions.add(new Extension() - .setUrl(CaseReporting.VALUESETAUTHOREXTENSIONURL) - .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETAUTHOR))); - extensions.add(new Extension() - .setUrl(CaseReporting.VALUESETSTEWARDEXTENSIONURL) - .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETSTEWARD))); - logger.info("Generating Condition Groupers..."); for (ConditionGrouperEntry conditionGrouperEntry : conditionGroupingEntries) { if (conditionGroupers.stream().noneMatch(cg -> cg.getTitle() @@ -410,7 +407,7 @@ private List generateConditionGroupers(List con } ValueSet conditionGrouperValueSet = new ValueSet(); - conditionGrouperValueSet.setExtension(extensions); + conditionGrouperValueSet.setExtension(createBaseExtensions()); conditionGrouperValueSet.setMeta(new Meta() .addProfile("http://aphl.org/fhir/vsm/StructureDefinition/vsm-conditiongroupervalueset")); conditionGrouperValueSet @@ -426,6 +423,21 @@ private List generateConditionGroupers(List con CaseReporting.SEARCHPARAMUSECONTEXTVALUEGROUPERTYPECONDITIONGROUPER, null); conditionGrouperValueSet.setUrl(canonicalId); + CurationCoverageLevelEntry coverage = resolveCoverage( + canonicalId, + conditionGrouperEntry.getConditionGrouperTitle(), + conditionGrouperCurationCoverageLevelByUrlMap, + conditionGrouperCurationCoverageLevelByTitleMap + ); + + if (coverage != null + && conditionGrouperValueSet.getExtension().stream() + .noneMatch(e -> "http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-curationCoverageLevel" + .equals(e.getUrl()))) { + + conditionGrouperValueSet.addExtension(buildCurationCoverageExtension(coverage)); + } + conditionGrouperValueSet.setVersion(this.version); conditionGrouperValueSet.setName(namify(conditionGrouperEntry.getConditionGrouperTitle())); conditionGrouperValueSet.setTitle(conditionGrouperEntry.getConditionGrouperTitle()); @@ -531,12 +543,8 @@ private List processAdditionalContextGrouperSheet row, CaseReporting.ADDITIONALCONTEXTGROUPERTARGETCONDITIONGROUPERTITLECOLINDEX, evaluator); String additionalContextGrouperUrl = SpreadsheetHelper.getCellAsStringEmptyForNull( row, CaseReporting.ADDITIONALCONTEXTGROUPERGENERATEDURLCOLINDEX, evaluator); - - // row.getCell(CaseReporting.ADDITIONALCONTEXTGROUPERTITLECOLINDEX).setCellType(CellType.STRING); String additionalContextGrouperTitle = SpreadsheetHelper.getCellAsStringEmptyForNull( row, CaseReporting.ADDITIONALCONTEXTGROUPERTITLECOLINDEX, evaluator); - // - // StringUtils.trimToEmpty(row.getCell(CaseReporting.ADDITIONALCONTEXTGROUPERTITLECOLINDEX).getStringCellValue().replace("\n", " ").replace('\u00A0', ' ')); // ValueSet references String additionalContextGrouperValueSetUrl = SpreadsheetHelper.getCellAsStringEmptyForNull( @@ -586,14 +594,6 @@ private List generateAdditionalContextGroupers( List additionalContextGrouperEntries) { List additionalContextGroupers = new ArrayList<>(); - List extensions = new ArrayList<>(); - extensions.add(new Extension() - .setUrl(CaseReporting.VALUESETAUTHOREXTENSIONURL) - .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETAUTHOR))); - extensions.add(new Extension() - .setUrl(CaseReporting.VALUESETSTEWARDEXTENSIONURL) - .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETSTEWARD))); - for (AdditionalContextGrouperEntry additionalContextGrouperEntry : additionalContextGrouperEntries) { // Get the existing RS Grouper if it exists // If it does not exist, create it @@ -616,17 +616,27 @@ private List generateAdditionalContextGroupers( } var vs = new ValueSet(); - vs.setExtension(extensions); + vs.getExtension().addAll(createBaseExtensions()); vs.setUrl(canonicalUrl); + + var coverage = resolveCoverage( + canonicalUrl, + additionalContextGrouperEntry.getAdditionalContextGrouperTitle(), + additionalContextGrouperCurationCoverageLevelByUrlMap, + additionalContextGrouperCurationCoverageLevelByTitleMap + ); + + if (coverage != null + && vs.getExtension().stream() + .noneMatch(e -> "http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-curationCoverageLevel" + .equals(e.getUrl()))) { + + vs.addExtension(buildCurationCoverageExtension(coverage)); + } + vs.setVersion(this.version); - vs.setName(namify(additionalContextGrouperEntry - .getAdditionalContextGrouperTitle() - .replace('\u00A0', ' ')) - .trim()); - vs.setTitle(additionalContextGrouperEntry - .getAdditionalContextGrouperTitle() - .replace('\u00A0', ' ') - .trim()); + vs.setName(namify(normalize(additionalContextGrouperEntry.getAdditionalContextGrouperTitle()))); + vs.setTitle(normalize(additionalContextGrouperEntry.getAdditionalContextGrouperTitle())); vs.setDescription( "The set of codes and value sets for artifacts that provide additional context in a report from a triggering event. (NOTE: Generated Content)"); vs.setStatus(Enumerations.PublicationStatus.ACTIVE); @@ -655,7 +665,7 @@ private List generateAdditionalContextGroupers( compose.setInclude(includes); } - addValueSetToComposeIfMissing(compose, additionalContextGrouperEntry.additionalContextGrouperValueSetUrl); + addValueSetToComposeIfMissing(compose, additionalContextGrouperEntry.getAdditionalContextGrouperValueSetUrl()); var relevantInclude = includes.stream() .filter(i -> i.hasSystem() @@ -737,16 +747,32 @@ private void addReportingSpecificationGrouperReferencesToConditionGroupers( private Library generateManifest(List components, List dependencies) { Library manifest = new Library(); + Parameters expParams = new Parameters(); + expParams.setId("exp-params"); + + expParams.addParameter() + .setName("activeOnly") + .setValue(new StringType("false")); + + manifest.addContained(expParams); + if (releaseLabel != null && !releaseLabel.isEmpty()) { manifest.addExtension(new Extension() - .setUrl("http://hl7.org/fhir/StructureDefinition/artifact-releaseLabel") - .setValue(new StringType(releaseLabel))); + .setUrl("http://hl7.org/fhir/StructureDefinition/artifact-releaseLabel") + .setValue(new StringType(releaseLabel))); } + + var expansionParametersExtension = new Extension( + "http://hl7.org/fhir/StructureDefinition/cqf-expansionParameters", + new Reference("#exp-params") + ); + manifest.addExtension(expansionParametersExtension); manifest.setUrl(CaseReporting.MANIFESTURL); - manifest.setVersion(this.version); + // Intentionally producing a draft manifest. It will be released and packaged downstream in the workflow. + manifest.setVersion(this.version + "-draft"); manifest.setName("TESContentLibrary"); manifest.setTitle("TES Content Library"); - manifest.setStatus(Enumerations.PublicationStatus.ACTIVE); + manifest.setStatus(Enumerations.PublicationStatus.DRAFT); manifest.setExperimental(false); manifest.setType(new CodeableConcept( new Coding("http://terminology.hl7.org/CodeSystem/library-type", "asset-collection", null))); @@ -777,19 +803,19 @@ private Library generateManifest(List components, List depen relatedArtifactsToAdd.add(componentRelatedArtifact); } - for (ValueSet component : components) { - var dependencyRelatedArtifact = - new RelatedArtifact().setType(RelatedArtifact.RelatedArtifactType.DEPENDSON); - dependencyRelatedArtifact.setResource(component.getUrl() + "|" + component.getVersion()); - relatedArtifactsToAdd.add(dependencyRelatedArtifact); - } - - for (ValueSet dependency : dependencies) { - var dependencyRelatedArtifact = - new RelatedArtifact().setType(RelatedArtifact.RelatedArtifactType.DEPENDSON); - dependencyRelatedArtifact.setResource(dependency.getUrl() + "|" + dependency.getVersion()); - relatedArtifactsToAdd.add(dependencyRelatedArtifact); - } +// for (ValueSet component : components) { +// var dependencyRelatedArtifact = +// new RelatedArtifact().setType(RelatedArtifact.RelatedArtifactType.DEPENDSON); +// dependencyRelatedArtifact.setResource(component.getUrl() + "|" + component.getVersion()); +// relatedArtifactsToAdd.add(dependencyRelatedArtifact); +// } +// +// for (ValueSet dependency : dependencies) { +// var dependencyRelatedArtifact = +// new RelatedArtifact().setType(RelatedArtifact.RelatedArtifactType.DEPENDSON); +// dependencyRelatedArtifact.setResource(dependency.getUrl() + "|" + dependency.getVersion()); +// relatedArtifactsToAdd.add(dependencyRelatedArtifact); +// } manifest.setRelatedArtifact(relatedArtifactsToAdd); @@ -806,6 +832,133 @@ private Library generateManifest(List components, List depen return manifest; } + // Curation Coverage Level Processing + private void loadCurationCoverage(Workbook workbook) { + processCoverageSheet( + workbook.getSheetAt(CaseReporting.CONDITIONGROUPERCURATIONCOVERAGELEVELSHEETINDEX), + conditionGrouperCurationCoverageLevelByUrlMap, + conditionGrouperCurationCoverageLevelByTitleMap + ); + + processCoverageSheet( + workbook.getSheetAt(CaseReporting.ADDITIONALCONTEXTGROUPERCURATIONCOVERAGELEVELSHEETINDEX), + additionalContextGrouperCurationCoverageLevelByUrlMap, + additionalContextGrouperCurationCoverageLevelByTitleMap + ); + } + + private void processCoverageSheet(Sheet sheet, + Map coverageByUrlMap, + Map coverageByTitleMap) { + Map map = new HashMap<>(); + + Iterator rowIterator = sheet.iterator(); + + // skip info + header + rowIterator.next(); + rowIterator.next(); + + FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator(); + + while (rowIterator.hasNext()) { + Row row = rowIterator.next(); + + String url = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 0, evaluator).trim(); + String title = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 1, evaluator).trim(); + String level = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 2, evaluator).trim(); + String levelReason = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 3, evaluator).trim(); + String author = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 4, evaluator).trim(); + String dateTime = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 5, evaluator).trim(); + String note = SpreadsheetHelper.getCellAsStringEmptyForNull(row, 6, evaluator).trim(); + + if ((StringUtils.isBlank(url) && StringUtils.isBlank(title)) || StringUtils.isBlank(level)) { + continue; + } + + level = level.toLowerCase(); + + if ("partial".equals(level) && StringUtils.isBlank(levelReason)) { + throw new IllegalArgumentException( + "CurationCoverageLevel 'partial' requires a reason for URL: " + StringUtils.defaultIfBlank(url, title) + ); + } + + CurationCoverageLevelEntry ccl = new CurationCoverageLevelEntry(level, levelReason, author, dateTime, note); + + if (StringUtils.isNotBlank(url)) { + coverageByUrlMap.put(url.trim(), ccl); + } + + if (StringUtils.isNotBlank(title)) { + coverageByTitleMap.put(normalize(title), ccl); + } + } + } + + private Extension buildCurationCoverageExtension(CurationCoverageLevelEntry data) { + Extension ext = new Extension(); + ext.setUrl("http://hl7.org/fhir/uv/crmi/StructureDefinition/crmi-curationCoverageLevel"); + + if (StringUtils.isNotBlank(data.getLevel())) { + CodeableConcept cc = new CodeableConcept(); + cc.addCoding(new Coding() + .setSystem("http://terminology.hl7.org/CodeSystem/curation-coverage-level-codes") + .setCode(data.getLevel())); + + ext.addExtension(new Extension() + .setUrl("level") + .setValue(cc)); + } + + if (StringUtils.isNotBlank(data.getLevelReason())) { + ext.addExtension(new Extension() + .setUrl("levelReason") + .setValue(new MarkdownType(data.getLevelReason()))); + } + + if (StringUtils.isNotBlank(data.getAuthor())) { + ext.addExtension(new Extension() + .setUrl("author") + .setValue(new StringType(data.getAuthor()))); + } + + if (StringUtils.isNotBlank(data.getDateTime())) { + Date parsedDate = parseDate(data.getDateTime()); + + if (parsedDate != null) { + ext.addExtension(new Extension() + .setUrl("dateTime") + .setValue(new DateTimeType(parsedDate))); + } + } + + if (StringUtils.isNotBlank(data.getNote())) { + ext.addExtension(new Extension() + .setUrl("note") + .setValue(new MarkdownType(data.getNote()))); + } + + return ext; + } + + private CurationCoverageLevelEntry resolveCoverage( + String url, + String title, + Map byUrl, + Map byTitle + ) { + if (StringUtils.isNotBlank(url)) { + CurationCoverageLevelEntry ccl = byUrl.get(url.trim()); + if (ccl != null) return ccl; + } + + if (StringUtils.isNotBlank(title)) { + return byTitle.get(normalize(title)); + } + + return null; + } + // Validation private void runSimpleValidation(List conditionGroupers) { for (ValueSet vs : conditionGroupers) { @@ -839,7 +992,7 @@ private void validateRSGroupers(List rsGroupers) { } else { if (logger.isInfoEnabled()) { logger.info( - "ValueSet '{}' has been validated and processed as a valid Reporting Specifciation Grouper", + "ValueSet '{}' has been validated and processed as a valid Reporting Specification Grouper", rsGrouper.getUrl()); } } @@ -847,6 +1000,20 @@ private void validateRSGroupers(List rsGroupers) { } // Helpers + private List createBaseExtensions() { + List extensions = new ArrayList<>(); + + extensions.add(new Extension() + .setUrl(CaseReporting.VALUESETAUTHOREXTENSIONURL) + .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETAUTHOR))); + + extensions.add(new Extension() + .setUrl(CaseReporting.VALUESETSTEWARDEXTENSIONURL) + .setValue(new ContactDetail().setName(CaseReporting.GROUPERVALUESETSTEWARD))); + + return extensions; + } + private static String namify(String input) { if (input == null || input.trim().isEmpty()) { throw new IllegalArgumentException("Input string cannot be null or empty"); @@ -864,6 +1031,25 @@ private static String namify(String input) { return sanitized; } + private String normalize(String input) { + if (input == null) return null; +// return input.replace('\u00A0', ' ').trim(); + return input.replace('\u00A0', ' ') // non-breaking space → normal space + .replace('\u0009', ' ') // tab → space + .replaceAll("\\s+", " ") // collapse all whitespace (incl. newlines, multiple spaces) + .trim(); + } + + private static Date parseDate(String input) { + if (StringUtils.isBlank(input)) { + return null; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yy"); + LocalDate localDate = LocalDate.parse(input.trim(), formatter); + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + private static List extractFlatCodeListFromValueSet(ValueSet conditionCodeValueSet) { List conditionCodeValueSetCodeAndDisplay = new ArrayList<>(); @@ -996,44 +1182,23 @@ private ValueSet loadConditionCodeValueSet(String pathToConditionCodeValueSet) { return conditionCodeValueSet; } - private void writeGeneratedGrouperUrlsToWorkbook(List generatedGroupers) { - // Create a new workbook and sheet + private void writeGeneratedGrouperUrlsToWorkbook( + List conditionGroupers, + List additionalContextGroupers) { + String filePath = this.getOutputPath() + "/generated-grouper-urls.xlsx"; Workbook workbook = new XSSFWorkbook(); - // Generate a Sheet with list of URLs that were generated for generated RS Groupers - Sheet sheet = workbook.createSheet("Generated Grouper URLs"); - - int rowCounter = 0; - Row headerRow = sheet.createRow(rowCounter); - Cell headerSystemCell = headerRow.createCell(0); - headerSystemCell.setCellValue("Url"); - Cell headerCodeCell = headerRow.createCell(1); - headerCodeCell.setCellValue("Grouper Title"); - rowCounter++; - - for (ValueSet vs : generatedGroupers) { - var includes = vs.getCompose().getInclude(); - var codeCount = 0; - for (ValueSet.ConceptSetComponent component : includes) { - codeCount += component.getConcept().size(); - } + try { + writeGrouperSheet(workbook, "Condition Groupers", conditionGroupers); + writeGrouperSheet(workbook, "AC Groupers", additionalContextGroupers); - for (int i = 0; i < codeCount; i++) { - Row row = sheet.createRow(rowCounter); - Cell systemCell = row.createCell(0); - systemCell.setCellValue(vs.getUrl()); - Cell codeCell = row.createCell(1); - codeCell.setCellValue(vs.getTitle()); - rowCounter++; - } - } + try (FileOutputStream fileOut = new FileOutputStream(filePath)) { + workbook.write(fileOut); - // Save the workbook to the file system - try (FileOutputStream fileOut = new FileOutputStream(filePath)) { - workbook.write(fileOut); - if (logger.isInfoEnabled()) { - logger.info("Excel file written successfully."); + if (logger.isInfoEnabled()) { + logger.info("Excel file written successfully."); + } } } catch (IOException e) { if (logger.isErrorEnabled()) { @@ -1050,6 +1215,41 @@ private void writeGeneratedGrouperUrlsToWorkbook(List generatedGrouper } } + private void writeGrouperSheet(Workbook workbook, String sheetName, List groupers) { + Sheet sheet = workbook.createSheet(sheetName); + + int rowCounter = 0; + + Row headerRow = sheet.createRow(rowCounter++); + headerRow.createCell(0).setCellValue("Url"); + headerRow.createCell(1).setCellValue("Grouper Title"); + + for (ValueSet vs : groupers) { + + int repeatCount = 1; + + if (vs.hasCompose() && vs.getCompose().hasInclude()) { + int codeCount = 0; + + for (ValueSet.ConceptSetComponent component : vs.getCompose().getInclude()) { + codeCount += component.getConcept().size(); + } + + // Preserve duplicate rows to align with workbook rows + if (codeCount > 0) { + repeatCount = codeCount; + } + } + + for (int i = 0; i < repeatCount; i++) { + Row row = sheet.createRow(rowCounter++); + + row.createCell(0).setCellValue(vs.getUrl()); + row.createCell(1).setCellValue(vs.getTitle()); + } + } + } + private void writeDifferencesToSheet(List baseList, List comparisonList, Sheet sheet) { int rowCounter = 0; Row headerRow = sheet.createRow(rowCounter); @@ -1087,129 +1287,4 @@ private void writeDifferencesToSheet(List baseList, List compari } } } - - // Model - private static class TESGrouperEntry { - private String conditionGrouperUrl; - - public String getConditionGrouperUrl() { - return conditionGrouperUrl; - } - - private String conditionGrouperTitle; - - public String getConditionGrouperTitle() { - return conditionGrouperTitle; - } - - public TESGrouperEntry(String conditionGrouperUrl, String conditionGrouperTitle) { - this.conditionGrouperUrl = conditionGrouperUrl; - this.conditionGrouperTitle = conditionGrouperTitle; - } - } - - private static class ConditionGrouperEntry extends TESGrouperEntry { - - private String reportingSpecificationGrouperTitle; - - public String getReportingSpecificationGrouperTitle() { - return reportingSpecificationGrouperTitle; - } - - private String reportingSpecificationConditionCode; - - public String getReportingSpecificationConditionCode() { - return reportingSpecificationConditionCode; - } - - private String reportingSpecificationConditionDescription; - - public String getReportingSpecificationConditionDescription() { - return reportingSpecificationConditionDescription; - } - - public ConditionGrouperEntry( - String conditionGrouperUrl, - String conditionGrouperTitle, - String reportingSpecificationGrouperTitle, - String reportingSpecificationConditionCode, - String reportingSpecificationConditionDescription) { - super(conditionGrouperUrl, conditionGrouperTitle); - this.reportingSpecificationGrouperTitle = reportingSpecificationGrouperTitle; - this.reportingSpecificationConditionCode = reportingSpecificationConditionCode; - this.reportingSpecificationConditionDescription = reportingSpecificationConditionDescription; - } - } - - private static class AdditionalContextGrouperEntry extends TESGrouperEntry { - private String additionalContextGrouperUrl; - - public String getAdditionalContextGrouperUrl() { - return additionalContextGrouperUrl; - } - - private String additionalContextGrouperTitle; - - public String getAdditionalContextGrouperTitle() { - return additionalContextGrouperTitle; - } - - private String additionalContextGrouperValueSetUrl; - - public String getAdditionalContextGrouperValueSetUrl() { - return additionalContextGrouperValueSetUrl; - } - - private String additionalContextGrouperValueSetTitle; - - public String getAdditionalContextGrouperValueSetTitle() { - return additionalContextGrouperValueSetTitle; - } - - private String additionalContextGrouperValueSetCodeSystem; - - public String getAdditionalContextGrouperValueSetCodeSystem() { - return additionalContextGrouperValueSetCodeSystem; - } - - private String additionalContextGrouperCode; - - public String getAdditionalContextGrouperCode() { - return additionalContextGrouperCode; - } - - private String additionalContextGrouperCodeDisplay; - - public String getAdditionalContextGrouperCodeDisplay() { - return additionalContextGrouperCodeDisplay; - } - - private String additionalContextGrouperCodeSystemUrl; - - public String getAdditionalContextGrouperCodeSystemUrl() { - return additionalContextGrouperCodeSystemUrl; - } - - public AdditionalContextGrouperEntry( - String conditionGrouperUrl, - String conditionGrouperTitle, - String acGrouperCanonicalUrl, - String additionalContextGrouperTitle, - String additionalContextGrouperValueSetUrl, - String additionalContextGrouperValueSetTitle, - String additionalContextGrouperValueSetCodeSystem, - String additionalContextGrouperCode, - String additionalContextGrouperCodeDisplay, - String additionalContextGrouperCodeSystemUrl) { - super(conditionGrouperUrl, conditionGrouperTitle); - this.additionalContextGrouperUrl = acGrouperCanonicalUrl; - this.additionalContextGrouperTitle = additionalContextGrouperTitle; - this.additionalContextGrouperValueSetUrl = additionalContextGrouperValueSetUrl; - this.additionalContextGrouperValueSetTitle = additionalContextGrouperValueSetTitle; - this.additionalContextGrouperValueSetCodeSystem = additionalContextGrouperValueSetCodeSystem; - this.additionalContextGrouperCode = additionalContextGrouperCode; - this.additionalContextGrouperCodeDisplay = additionalContextGrouperCodeDisplay; - this.additionalContextGrouperCodeSystemUrl = additionalContextGrouperCodeSystemUrl; - } - } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/constants/CaseReporting.java b/tooling/src/main/java/org/opencds/cqf/tooling/constants/CaseReporting.java index 768382600..ca1119b62 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/constants/CaseReporting.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/constants/CaseReporting.java @@ -29,18 +29,11 @@ private CaseReporting() {} public static final int REPORTINGSPECIFICATIONCONDITIONCODECOLINDEX = 5; public static final int REPORTINGSPECIFICATIONCONDITIONDESCRIPTIONCOLINDEX = 6; - // Additional Context Groupers Sheet Layout - // public static final int ADDITIONALCONTEXTGROUPERSHEETINDEX = 2; - // public static final int ADDITIONALCONTEXTGROUPERTARGETCONDITIONGROUPERURLCOLINDEX = 0; - // public static final int ADDITIONALCONTEXTGROUPERTARGETCONDITIONGROUPERTITLECOLINDEX = 1; - // public static final int ADDITIONALCONTEXTGROUPERURLCOLINDEX = 2; - // public static final int ADDITIONALCONTEXTGROUPERTITLECOLINDEX = 3; - // public static final int ADDITIONALCONTEXTGROUPERVALUESETTITLEINDEX = 4; - // public static final int ADDITIONALCONTEXTGROUPERVALUESETURLINDEX = 5; - // public static final int ADDITIONALCONTEXTGROUPERVALUESETSYSTEMINDEX = 6; + // Condition Grouper Curation Coverage Level + public static final int CONDITIONGROUPERCURATIONCOVERAGELEVELSHEETINDEX = 2; - // Additional Context Groupers Codes Sheet Layout - public static final int ADDITIONALCONTEXTGROUPERSHEETINDEX = 2; + // Additional Context Groupers Sheet Layout + public static final int ADDITIONALCONTEXTGROUPERSHEETINDEX = 3; public static final int ADDITIONALCONTEXTGROUPERTARGETCONDITIONGROUPERURLCOLINDEX = 0; public static final int ADDITIONALCONTEXTGROUPERTARGETCONDITIONGROUPERTITLECOLINDEX = 1; public static final int ADDITIONALCONTEXTGROUPERGENERATEDURLCOLINDEX = 2; @@ -52,4 +45,7 @@ private CaseReporting() {} public static final int ADDITIONALCONTEXTGROUPERCODECOLINDEX = 8; public static final int ADDITIONALCONTEXTGROUPERCODEDISPLAYCOLINDEX = 9; public static final int ADDITIONALCONTEXTGROUPERCODESYSTEMURLCOLINDEX = 10; + + // Additional Context Grouper Curation Coverage Level + public static final int ADDITIONALCONTEXTGROUPERCURATIONCOVERAGELEVELSHEETINDEX = 4; }