From 87e21dbf4f52b1b9f431f83b9b520203a09a84ce Mon Sep 17 00:00:00 2001 From: Bryn Rhodes Date: Fri, 13 Aug 2021 07:19:45 -0600 Subject: [PATCH] #277: Added EnsureComputableValueSet operation. --- .../java/org/opencds/cqf/tooling/Main.java | 13 ++ .../opencds/cqf/tooling/OperationFactory.java | 1 + .../acceleratorkit/DictionaryCode.java | 14 ++ .../cqf/tooling/acceleratorkit/Processor.java | 163 ++++++++++++++---- .../EnsureExecutableValueSetOperation.java | 127 +++++++++++++- 5 files changed, 278 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/opencds/cqf/tooling/Main.java b/src/main/java/org/opencds/cqf/tooling/Main.java index 6b815b417..1486037f7 100644 --- a/src/main/java/org/opencds/cqf/tooling/Main.java +++ b/src/main/java/org/opencds/cqf/tooling/Main.java @@ -176,6 +176,19 @@ - VmrToFhirTransformer - command: mvn exec: java -Dexec.args="-VmrToFhir -ifp=./src/test/resources/org/opencds/cqf/tooling/operation/VmrToFhir -op=./src/test/resources/org/opencds/cqf/tooling/operation/VmrToFhir/vMROutput.xml -e=xml" - this tooling transforms vMR data to FHIR data + + - EnsureExecutableValueSet + - command: mvn exec: java -Dexec.args="-EnsureExecutableValueSet [-valuesetpath | -vsp] (-outputpath | -op) (-declarecpg | -cpg) (-force | -f)" + - This tooling generates an expansion if one is not present (and the compose consists only of includes without filters) + - The -cpg flag indicates whether to mark the value set as executable with CPG profile indicators + - The -force flag indicates that even if the value set has an expansion, this should recompute it + + - EnsureComputableValueSet + - command: mvn exec: java -Dexec.args="-EnsureComputableValueSet [-valuesetpath | -vsp] (-outputpath | -op) (-declarecpg | -cpg) (-force | -f) (-skipversion | -sv)" + - This tooling infers a compose if one is not present (and there is an expansion) + - The -cpg flag indicates whether to mark the value set as computable with CPG profile indicators + - The -force flag indicates that even if the value set has a compose, this should reinfer it + - The -skipversion flag indicates that code system versions that are present in the expansion should not be expressed in the inferred compose */ public class Main { diff --git a/src/main/java/org/opencds/cqf/tooling/OperationFactory.java b/src/main/java/org/opencds/cqf/tooling/OperationFactory.java index 71771fe93..f793b2c36 100644 --- a/src/main/java/org/opencds/cqf/tooling/OperationFactory.java +++ b/src/main/java/org/opencds/cqf/tooling/OperationFactory.java @@ -38,6 +38,7 @@ static Operation createOperation(String operationName) { case "OpioidXlsxToValueSet": return new OpioidValueSetGenerator(); case "EnsureExecutableValueSet": + case "EnsureComputableValueSet": return new EnsureExecutableValueSetOperation(); case "ToJsonValueSetDb": return new ToJsonValueSetDbOperation(); diff --git a/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DictionaryCode.java b/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DictionaryCode.java index a3ab79a34..880f2be5c 100644 --- a/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DictionaryCode.java +++ b/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DictionaryCode.java @@ -11,6 +11,20 @@ */ public class DictionaryCode { + private String id; + public String getId() { + return this.id; + } + public void setId(String id) { + if (id == null) { + this.id = null; + } + + if (id != null) { + this.id = id.replace((char) 160, (char) 32).trim(); + } + } + private String label; public String getLabel() { return this.label; diff --git a/src/main/java/org/opencds/cqf/tooling/acceleratorkit/Processor.java b/src/main/java/org/opencds/cqf/tooling/acceleratorkit/Processor.java index 8820b4b7e..078c76de5 100644 --- a/src/main/java/org/opencds/cqf/tooling/acceleratorkit/Processor.java +++ b/src/main/java/org/opencds/cqf/tooling/acceleratorkit/Processor.java @@ -55,19 +55,23 @@ public void setCanonicalBase(String value) { // private Map fhirModelStructureDefinitions = new LinkedHashMap(); private Map elementMap = new LinkedHashMap(); + private Map elementsById = new HashMap<>(); private Map elementIds = new LinkedHashMap(); private Map activityMap = new LinkedHashMap(); private List profileExtensions = new ArrayList<>(); private List extensions = new ArrayList(); private List profiles = new ArrayList(); + private Map profilesByElementId = new HashMap(); private Map> elementsByProfileId = new LinkedHashMap>(); private Map> profilesByActivityId = new LinkedHashMap>(); private Map> profilesByParentProfile = new LinkedHashMap>(); private List codeSystems = new ArrayList(); private List questionnaires = new ArrayList(); private List valueSets = new ArrayList(); + private Map valueSetNameMap = new HashMap(); private Map conceptMaps = new LinkedHashMap(); private Map concepts = new LinkedHashMap(); + private Map conceptNameMap = new HashMap(); private List retrieves = new ArrayList(); private List igJsonFragments = new ArrayList(); private List igResourceFragments = new ArrayList(); @@ -225,6 +229,9 @@ private void processScope(Workbook workbook, String scope) { // attached the generated extensions to the profiles that reference them attachExtensions(); + // process questionnaires + processQuestionnaires(); + // write all resources writeExtensions(outputPath); writeProfiles(outputPath); @@ -550,7 +557,7 @@ private List cleanseCodes(List codes) { return codes; } - private List getTerminologyCodes(String codeSystemKey, String label, Row row, HashMap colIds) { + private List getTerminologyCodes(String codeSystemKey, String id, String label, Row row, HashMap colIds) { List codes = new ArrayList<>(); String system = supportedCodeSystems.get(codeSystemKey); String codeListString = SpreadsheetHelper.getCellAsString(row, getColId(colIds, codeSystemKey)); @@ -591,14 +598,14 @@ private List getTerminologyCodes(String codeSystemKey, String la break; } - codes.add(getCode(system, label, display, c, null)); + codes.add(getCode(system, id, label, display, c, null)); } } return cleanseCodes(codes); } - private List getFhirCodes(String label, Row row, HashMap colIds) { + private List getFhirCodes(String id, String label, Row row, HashMap colIds) { List codes = new ArrayList<>(); String system = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirCodeSystem")); // If this is an input option with a custom code, add codes for the input options @@ -610,12 +617,14 @@ private List getFhirCodes(String label, Row row, HashMap getFhirCodes(String label, Row row, HashMap codesList = Arrays.asList(codeListString.split(";")); for (String c : codesList) { - codes.add(getCode(system, label, display, c, null)); + codes.add(getCode(system, id, label, display, c, null)); } } @@ -661,7 +671,7 @@ private List getFhirCodes(String label, Row row, HashMap getOpenMRSCodes(String label, Row row, HashMap colIds) { + private List getOpenMRSCodes(String elementId, String elementLabel, Row row, HashMap colIds) { List codes = new ArrayList<>(); String system = openMRSSystem; String parent = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "OpenMRSEntityParent")); @@ -671,20 +681,21 @@ private List getOpenMRSCodes(String label, Row row, HashMap codesList = Arrays.asList(codeListString.split(";")); for (String c : codesList) { - codes.add(getCode(system, label, display, c, parent)); + codes.add(getCode(system, elementId, elementLabel, display, c, parent)); } } return cleanseCodes(codes); } - private List getPrimaryCodes(String label, Row row, HashMap colIds) { + private List getPrimaryCodes(String elementId, String elementLabel, Row row, HashMap colIds) { List codes; - codes = getDataElementCodes(row, colIds, label); + codes = getDataElementCodes(row, colIds, elementId, elementLabel); return codes; } - private DictionaryCode getCode(String system, String label, String display, String codeValue, String parent) { + private DictionaryCode getCode(String system, String id, String label, String display, String codeValue, String parent) { DictionaryCode code = new DictionaryCode(); + code.setId(id); code.setLabel(label); code.setSystem(system); code.setDisplay(display); @@ -769,7 +780,7 @@ private DictionaryElement createDataElement(String page, String group, Row row, e.setContext(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Context"))); e.setSelector(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Selector"))); //TODO: Get all codes specified on the element, create a valueset and bind to it. Required - e.setPrimaryCodes(getPrimaryCodes(name, row, colIds)); + e.setPrimaryCodes(getPrimaryCodes(id, name, row, colIds)); DictionaryFhirElementPath fhirElementPath = getFhirElementPath(row, colIds); if (fhirElementPath != null) { @@ -790,9 +801,10 @@ private DictionaryElement createDataElement(String page, String group, Row row, } private void addInputOptionToParentElement(Row row, HashMap colIds) { + String parentId = getDataElementID(currentInputOptionParentRow, colIds).trim(); String parentName = getDataElementLabel(currentInputOptionParentRow, colIds).trim(); - if (parentName != null && !parentName.isEmpty()) + if ((parentId != null && !parentId.isEmpty()) || (parentName != null && !parentName.isEmpty())) { DictionaryElement parentElement = elementMap.get(parentName); if (parentElement != null) { @@ -823,8 +835,15 @@ private void addInputOptionToParentElement(Row row, HashMap col inputOptionValueSetName = inputOptionValueSetName + " Choices"; } + String optionId = getDataElementID(row, colIds); String optionLabel = getDataElementLabel(row, colIds); - List inputOptionCodes = getDataElementCodes(row, colIds, optionLabel != null && !optionLabel.isEmpty() ? optionLabel : parentName); + List inputOptionCodes = getDataElementCodes(row, colIds, + optionId != null && !optionId.isEmpty() ? optionId : parentId, + optionLabel != null && !optionLabel.isEmpty() ? optionLabel : parentName); + + if (!valueSetNameMap.containsKey(inputOptionValueSetName)) { + valueSetNameMap.put(inputOptionValueSetName, optionId); + } if (valueSetCodes.containsKey(inputOptionValueSetName)) { List entryCodes = valueSetCodes.get(inputOptionValueSetName); @@ -843,25 +862,25 @@ private void addInputOptionToParentElement(Row row, HashMap col } } - private List getDataElementCodes(Row row, HashMap colIds, String dataElementLabel) { + private List getDataElementCodes(Row row, HashMap colIds, String elementId, String elementLabel) { List codes = new ArrayList<>(); if (enableOpenMRS) { // Open MRS choices - List mrsCodes = getOpenMRSCodes(dataElementLabel, row, colIds); + List mrsCodes = getOpenMRSCodes(elementId, elementLabel, row, colIds); codes.addAll(mrsCodes); } // FHIR choices //String fhirCodeSystem = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirCodeSystem")); //if (fhirCodeSystem != null && !fhirCodeSystem.isEmpty()) { - List fhirCodes = getFhirCodes(dataElementLabel, row, colIds); + List fhirCodes = getFhirCodes(elementId, elementLabel, row, colIds); codes.addAll(fhirCodes); //} // Other Terminology choices for (String codeSystemKey : supportedCodeSystems.keySet()) { - List codeSystemCodes = getTerminologyCodes(codeSystemKey, dataElementLabel, row, colIds); + List codeSystemCodes = getTerminologyCodes(codeSystemKey, elementId, elementLabel, row, colIds); if (codes != codeSystemCodes && !codeSystemCodes.isEmpty()) { for (DictionaryCode c : codes) { c.getMappings().addAll(codeSystemCodes); @@ -1050,6 +1069,7 @@ else if (row.getRowNum() == headerRow) { DictionaryElement e = createDataElement(page, currentGroup, row, colIds); if (e != null) { elementMap.put(e.getName(), e); + elementsById.put(e.getId(), e); updateQuestionnaireForDataElement(e, questionnaire); } break; @@ -1071,7 +1091,8 @@ else if (row.getRowNum() == headerRow) { private Questionnaire createQuestionnaireForPage(Sheet sheet) { Questionnaire questionnaire = new Questionnaire(); - questionnaire.setId(toId(getActivityCoding(sheet.getSheetName()).getCode())); + Coding activityCoding = getActivityCoding(sheet.getSheetName()); + questionnaire.setId(toUpperId(activityCoding.getCode())); questionnaire.getExtension().add( new Extension("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability", new CodeType("shareable"))); @@ -1090,7 +1111,7 @@ private Questionnaire createQuestionnaireForPage(Sheet sheet) { questionnaire.setDescription("TODO: description goes here"); Coding useContextCoding = new Coding("http://terminology.hl7.org/CodeSystem/usage-context-type", "task", "Workflow Task"); - CodeableConcept useContextValue = new CodeableConcept(new Coding("http://fhir.org/guides/who/anc-cds/CodeSystem/activity-codes", questionnaire.getId(), sheet.getSheetName())); + CodeableConcept useContextValue = new CodeableConcept(new Coding(activityCoding.getSystem(), activityCoding.getCode(), activityCoding.getDisplay())); UsageContext useContext = new UsageContext(useContextCoding, useContextValue); questionnaire.getUseContext().add(useContext); @@ -1164,7 +1185,7 @@ private Questionnaire.QuestionnaireItemType getQuestionnaireItemType(DictionaryE private void updateQuestionnaireForDataElement(DictionaryElement dataElement, Questionnaire questionnaire) { Questionnaire.QuestionnaireItemComponent questionnaireItem = new Questionnaire.QuestionnaireItemComponent(); questionnaireItem.setLinkId(String.valueOf(questionnaireItemLinkIdCounter)); - String definition = String.format("%s/StructureDefinition/%s", canonicalBase, dataElement.getId().toLowerCase()); + String definition = dataElement.getId(); questionnaireItem.setDefinition(definition); questionnaireItem.setText(dataElement.getDataElementLabel()); Questionnaire.QuestionnaireItemType questionnaireItemType = getQuestionnaireItemType(dataElement); @@ -1230,6 +1251,29 @@ private boolean isMultipleChoiceElement(DictionaryElement element) { } } + private String toUpperId(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + + if (name.endsWith(".")) { + name = name.substring(0, name.lastIndexOf(".")); + } + + return name.trim() + // remove these characters + .replace("(", "").replace(")", "").replace("[", "").replace("]", "").replace("\n", "") + .replace(":", "") + .replace(",", "") + .replace("_", "") + .replace("/", "") + .replace(" ", "") + .replace(".", "") + .replace("-", "") + .replace(">", "") + .replace("<", ""); + } + private String toId(String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Name cannot be null or empty"); @@ -1630,6 +1674,9 @@ private void applyDataElementToElementDefinition(DictionaryElement element, Stru lde.add(element); } } + + // Record the profile in which the data element is present: + profilesByElementId.put(element.getId(), sd); } private void ensureProfile(DictionaryElement element) { @@ -1897,6 +1944,14 @@ private void ensureElement(DictionaryElement element, StructureDefinition sd) { } } + private String getValueSetId(String valueSetName) { + String id = valueSetNameMap.get(valueSetName); + if (id == null) { + id = valueSetName; + } + return toId(id); + } + private void ensureChoicesDataElement(DictionaryElement dictionaryElement, StructureDefinition sd) { if (dictionaryElement.getChoices() != null && dictionaryElement.getChoices().getFhirElementPath() != null) { String choicesElementId = dictionaryElement.getChoices().getFhirElementPath().getResourceTypeAndPath(); @@ -1912,15 +1967,17 @@ private void ensureChoicesDataElement(DictionaryElement dictionaryElement, Struc List valueSets = new ArrayList(); for (Map.Entry> vs: valueSetCodes.entrySet()) { - ValueSet valueSet = ensureValueSetWithCodes(toId(vs.getKey()), vs.getKey(), new CodeCollection(vs.getValue())); + ValueSet valueSet = ensureValueSetWithCodes(getValueSetId(vs.getKey()), vs.getKey(), new CodeCollection(vs.getValue())); valueSets.add(valueSet); valueSetToBind = valueSet; } if (valueSetCodes != null && valueSetCodes.size() > 1) { - String choicesGrouperValueSetName = parentCustomValueSetName + " Choices Grouper"; - valueSetToBind = createGrouperValueSet(toId(choicesGrouperValueSetName), choicesGrouperValueSetName, valueSets); + String choicesGrouperValueSetName = parentCustomValueSetName + " Choices Grouper"; + String choicesGrouperValueSetId = dictionaryElement.getId() + "-choices-grouper"; + valueSetNameMap.put(choicesGrouperValueSetName, choicesGrouperValueSetId); + valueSetToBind = createGrouperValueSet(getValueSetId(choicesGrouperValueSetName), choicesGrouperValueSetName, valueSets); } //TODO: Include the primaryCodes valueset in the grouper. Add the codes to the VS in the single VS case. @@ -1975,7 +2032,7 @@ private void ensureChoicesDataElement(DictionaryElement dictionaryElement, Struc private void bindQuestionnaireItemAnswerValueSet(DictionaryElement dictionaryElement, ValueSet valueSetToBind) { Questionnaire questionnaire = - questionnaires.stream().filter(q -> q.getId().equalsIgnoreCase(toId(getActivityCoding(dictionaryElement.getPage()).getCode()))).findFirst().get(); + questionnaires.stream().filter(q -> q.getId().equalsIgnoreCase(toUpperId(getActivityCoding(dictionaryElement.getPage()).getCode()))).findFirst().get(); Questionnaire.QuestionnaireItemComponent questionnaireItem = questionnaire.getItem().stream().filter(i -> i.getText().equalsIgnoreCase(dictionaryElement.getLabel())).findFirst().get(); questionnaireItem.setAnswerValueSet(valueSetToBind.getUrl()); @@ -2067,6 +2124,7 @@ private void ensureTerminologyAndBindToElement(DictionaryElement dictionaryEleme // Observation.code is special case - if mapping is Observation.value[x] with a non-bindable type, we'll still need // to allow for binding of Observation.code (the primary code path) if (isBindableType(dictionaryElement) || targetElement.getPath().equals("Observation.code")) { + String valueSetId = toId(dictionaryElement.getId()); String valueSetLabel = dictionaryElement.getLabel(); String valueSetName = null; @@ -2090,10 +2148,10 @@ private void ensureTerminologyAndBindToElement(DictionaryElement dictionaryEleme codesToBind = dictionaryElement.getPrimaryCodes(); } - String valueSetId = toId(valueSetName); + valueSetNameMap.put(valueSetName, valueSetId); ValueSet valueSet = null; if (codesToBind != null) { - valueSet = ensureValueSetWithCodes(valueSetId, valueSetLabel, codesToBind); + valueSet = ensureValueSetWithCodes(getValueSetId(valueSetName), valueSetLabel, codesToBind); } if (valueSet != null) { @@ -2533,6 +2591,34 @@ public void writeCodeSystems(String scopePath) { } } + public void processQuestionnaires() { + for (Questionnaire q : questionnaires) { + for (Questionnaire.QuestionnaireItemComponent item : q.getItem()) { + if (item.hasDefinition()) { + String definition = item.getDefinition(); + DictionaryElement de = elementsById.get(definition); + if (de != null) { + StructureDefinition sd = profilesByElementId.get(de.getId()); + if (sd != null) { + if (de.getFhirElementPath() != null && de.getFhirElementPath().getResourcePath() != null) { + item.setDefinition(String.format("%s#%s", sd.getUrl(), de.getFhirElementPath().getResourcePath())); + } + else { + item.setDefinition(sd.getUrl()); + } + } + else { + item.setDefinition(null); + } + } + else { + item.setDefinition(null); + } + } + } + } + } + public void writeQuestionnaires(String scopePath) { if (questionnaires != null && questionnaires.size() > 0) { String questionnairePath = getQuestionnairePath(scopePath); @@ -3080,18 +3166,29 @@ private void writeActivityIndexHeader(StringBuilder activityIndex, String activi } activityIndex.append(System.lineSeparator()); activityIndex.append(System.lineSeparator()); - activityIndex.append("|Data Element Id|Data Element|"); + if (activityCoding != null) { + String questionnaireId = toUpperId(activityCoding.getCode()); + activityIndex.append(String.format("Data elements for this activity can be collected using the [%s](Questionnaire-%s.html)", questionnaireId, questionnaireId)); + activityIndex.append(System.lineSeparator()); + activityIndex.append(System.lineSeparator()); + } + activityIndex.append("|Id|Label|Description|Type|Profile Path|"); activityIndex.append(System.lineSeparator()); - activityIndex.append("|---|---|"); + activityIndex.append("|---|---|---|---|---|"); activityIndex.append(System.lineSeparator()); } private void writeActivityIndexEntry(StringBuilder activityIndex, StructureDefinition sd) { - Identifier dataElementIdentifier = getDataElementIdentifier(sd.getIdentifier()); - String title = sd.hasTitle() ? sd.getTitle() : sd.hasName() ? sd.getName() : sd.getId(); - activityIndex.append(String.format("|%s|[%s](StructureDefinition-%s.html)|", - dataElementIdentifier != null ? dataElementIdentifier.getValue() : "", title, sd.getId())); - activityIndex.append(System.lineSeparator()); + List lde = elementsByProfileId.get(sd.getId()); + if (lde != null) { + for (DictionaryElement de : lde) { + String path = de.getFhirElementPath() != null ? de.getFhirElementPath().getResourceTypeAndPath() : ""; + String type = de.getFhirElementPath() != null ? de.getFhirElementPath().getFhirElementType() : de.getType(); + activityIndex.append(String.format("|%s|%s|%s|%s|[%s](StructureDefinition-%s.html)|", + de.getId(), de.getDataElementLabel(), de.getDescription(), type, path, sd.getId())); + activityIndex.append(System.lineSeparator()); + } + } } public void writeDataElements(String scope, String scopePath) { diff --git a/src/main/java/org/opencds/cqf/tooling/terminology/EnsureExecutableValueSetOperation.java b/src/main/java/org/opencds/cqf/tooling/terminology/EnsureExecutableValueSetOperation.java index bada4a0bd..53a4cec18 100644 --- a/src/main/java/org/opencds/cqf/tooling/terminology/EnsureExecutableValueSetOperation.java +++ b/src/main/java/org/opencds/cqf/tooling/terminology/EnsureExecutableValueSetOperation.java @@ -2,21 +2,24 @@ import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.Extension; -import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.ValueSet; import org.opencds.cqf.tooling.Operation; import org.opencds.cqf.tooling.utilities.IOUtils; import java.io.File; import java.time.Instant; -import java.util.Date; +import java.util.*; public class EnsureExecutableValueSetOperation extends Operation { + private static final String USAGE_WARNING = "CAUTION: The compose element in this ValueSet resource was inferred from the expansion element. It is NOT an authoritative definition of the value set and is provided only for convenience for systems that assume a compose will be present."; private String valueSetPath; private String encoding = IOUtils.Encoding.JSON.toString(); private boolean declareCPGProfiles = true; + private boolean ensureExecutable = false; + private boolean ensureComputable = false; + private boolean force = false; + private boolean skipVersion = false; private FhirContext fhirContext; public FhirContext getFhirContext() { @@ -32,7 +35,15 @@ public void execute(String[] args) { setOutputPath("src/main/resources/org/opencds/cqf/tooling/terminology/output"); // default for (String arg : args) { - if (arg.equals("-EnsureExecutableValueSet")) continue; + if (arg.equals("-EnsureExecutableValueSet")) { + ensureExecutable = true; + force = true; // force behavior defaults to true for ensure executable + continue; + } + if (arg.equals("-EnsureComputableValueSet")) { + ensureComputable = true; + continue; + } String[] flagAndValue = arg.split("="); if (flagAndValue.length < 2) { throw new IllegalArgumentException("Invalid argument: " + arg); @@ -45,6 +56,8 @@ public void execute(String[] args) { case "valuesetpath": case "path": case "vsp": valueSetPath = value; break; // -valuesetpath (-vsp, -path) case "encoding": case "e": encoding = value.toLowerCase(); break; case "declarecpg": case "cpg": declareCPGProfiles = value.toLowerCase().equals("true") ? true : false; break; + case "force": case "f": force = value.toLowerCase().equals("true") ? true : false; break; + case "skipversion": case "sv": skipVersion = value.toLowerCase().equals("true") ? true : false; break; default: throw new IllegalArgumentException("Unknown flag: " + flag); } } @@ -58,7 +71,7 @@ public void execute(String[] args) { IBaseResource resource = IOUtils.readResource(file.getAbsolutePath(), getFhirContext()); if (resource instanceof ValueSet) { ValueSet valueSet = (ValueSet)resource; - if (refreshExpansion(valueSet)) { + if ((ensureExecutable && refreshExpansion(valueSet)) || (ensureComputable && inferCompose(valueSet))) { IOUtils.writeResource(valueSet, file.getAbsolutePath(), IOUtils.Encoding.parse(encoding), getFhirContext()); } } @@ -67,7 +80,7 @@ public void execute(String[] args) { } public boolean refreshExpansion(ValueSet valueSet) { - if (hasSimpleCompose(valueSet)) { + if (hasSimpleCompose(valueSet) && (!valueSet.hasExpansion() || force)) { ValueSet.ValueSetExpansionComponent expansion = new ValueSet.ValueSetExpansionComponent(); expansion.setTimestamp(Date.from(Instant.now())); for (ValueSet.ConceptSetComponent csc : valueSet.getCompose().getInclude()) { @@ -93,6 +106,106 @@ public boolean refreshExpansion(ValueSet valueSet) { return false; } + private String getSystemCanonicalReference(String system, String version) { + if (version != null) { + return system + "|" + version; + } + + return system; + } + + private String getSystemUri(String systemCanonicalReference) { + if (systemCanonicalReference != null) { + int i = systemCanonicalReference.indexOf("|"); + if (i > 0) { + return systemCanonicalReference.substring(0, i); + } + } + return systemCanonicalReference; + } + + private String getVersion(String systemCanonicalReference) { + if (systemCanonicalReference != null) { + int i = systemCanonicalReference.indexOf("|"); + if (i > 0) { + return systemCanonicalReference.substring(i + 1); + } + } + return null; + } + + // NOTE: This assumes the ValueSet expansion is complete... It will THROW if the expansion is partial + public boolean inferCompose(ValueSet valueSet) { + if (valueSet.hasExpansion() && (!valueSet.hasCompose() || force)) { + int total = valueSet.getExpansion().hasTotal() ? valueSet.getExpansion().getTotal() : -1; + int count = 0; + + // Index the expansion contains by code system and version + HashMap> codesBySystem = new HashMap>(); + for (ValueSet.ValueSetExpansionContainsComponent contains : valueSet.getExpansion().getContains()) { + count++; + if (contains.hasCode() && contains.hasSystem()) { + String scr = getSystemCanonicalReference(contains.getSystem(), !skipVersion ? contains.getVersion() : null); + Map concepts = codesBySystem.get(scr); + if (concepts == null) { + concepts = new LinkedHashMap(); + codesBySystem.put(scr, concepts); + } + if (!concepts.containsKey(contains.getCode())) { + concepts.put(contains.getCode(), contains); + } + } + } + + // Validate that we're dealing with a complete expansion + if (count < total) { + throw new IllegalArgumentException("Compose cannot be inferred from a partial expansion"); + } + + // Add a compose include for each system and version + ValueSet.ValueSetComposeComponent compose = new ValueSet.ValueSetComposeComponent(); + valueSet.setCompose(compose); + for (Map.Entry> conceptsEntry : codesBySystem.entrySet()) { + ValueSet.ConceptSetComponent csc = compose.addInclude(); + String systemUri = getSystemUri(conceptsEntry.getKey()); + String version = getVersion(conceptsEntry.getKey()); + csc.setSystem(systemUri); + if (version != null) { + csc.setVersion(version); + } + + for (ValueSet.ValueSetExpansionContainsComponent c : conceptsEntry.getValue().values()) { + csc.addConcept().setCode(c.getCode()).setDisplay(c.getDisplay()); + } + } + + // Mark CPG Profiles and knowledge capabilities + if (declareCPGProfiles) { + if (!valueSet.getMeta().hasProfile("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-computablevalueset")) { + valueSet.getMeta().addProfile("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-computablevalueset"); + } + ensureKnowledgeCapability(valueSet,"computable"); + ensureKnowledgeRepresentationLevel(valueSet,"structured"); + } + + // Add a warning that the compose element was inferred from the expansion + boolean hasCaution = false; + for (Extension e : valueSet.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-warning")) { + if (e.getValue() instanceof MarkdownType && ((MarkdownType)e.getValue()).hasValue() && ((MarkdownType)e.getValue()).getValue().equals(USAGE_WARNING)) { + hasCaution = true; + break; + } + } + if (!hasCaution) { + valueSet.addExtension("http://hl7.org/fhir/StructureDefinition/valueset-warning", new MarkdownType().setValue(USAGE_WARNING)); + } + + return true; + } + + return false; + } + public boolean hasKnowledgeCapability(DomainResource resource, String capability) { for (Extension e : resource.getExtension()) { if ("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability".equals(e.getUrl())) {