diff --git a/pom.xml b/pom.xml index 2e334c05e..931a70eac 100644 --- a/pom.xml +++ b/pom.xml @@ -16,8 +16,8 @@ 2.9.0 2.6.0 2.9.0 - 6.0.1 - 5.6.36 + 6.2.2 + 5.6.68 2.1.5.RELEASE diff --git a/tooling-cli/pom.xml b/tooling-cli/pom.xml index cc91e6e67..87b3b4f9b 100644 --- a/tooling-cli/pom.xml +++ b/tooling-cli/pom.xml @@ -21,7 +21,6 @@ org.opencds.cqf tooling 2.5.0-SNAPSHOT - @@ -35,6 +34,19 @@ org.eclipse.persistence.moxy 2.7.7 + + + org.reflections + reflections + 0.10.2 + + + + org.testng + testng + 7.7.0 + test + diff --git a/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/Main.java b/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/Main.java index 787b2ac65..f87744fe9 100644 --- a/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/Main.java +++ b/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/Main.java @@ -218,20 +218,55 @@ */ +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.exception.InvalidOperationInitialization; +import org.opencds.cqf.tooling.exception.OperationNotFound; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.reflections.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + public class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + private static Map> operationClassMap; public static void main(String[] args) { - if (args.length == 0) { - System.err.println("cqf-tooling version: " + Main.class.getPackage().getImplementationVersion()); - System.err.println("Requests must include which operation to run as a command line argument. See docs for examples on how to use this project."); - return; + if (args == null || args.length == 0) { + logger.error("cqf-tooling version: {}", Main.class.getPackage().getImplementationVersion()); + throw new OperationNotFound( + "Requests must include which operation to run as a command line argument. See docs for examples on how to use this project."); + } + + // NOTE: we may want to use the Spring Context Library to find the annotated classes + if (operationClassMap == null) { + operationClassMap = new HashMap<>(); + Reflections reflections = new Reflections("org.opencds.cqf.tooling.operations"); + Set> operationClasses = reflections + .getTypesAnnotatedWith(Operation.class); + operationClasses.forEach(clazz -> operationClassMap.put(clazz.getAnnotation(Operation.class).name(), clazz)); } String operation = args[0]; if (!operation.startsWith("-")) { - throw new IllegalArgumentException("Invalid operation: " + operation); + throw new InvalidOperationArgs( + "Invalid operation syntax: " + operation + ". Operations must be declared with a \"-\" prefix"); } - OperationFactory.createOperation(operation.substring(1)).execute(args); + try { + ExecutableOperation executableOperation = OperationFactory.createOperation( + operation, operationClassMap.get(operation.substring(1)), args); + if (executableOperation != null) { + executableOperation.execute(); + } + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + throw new InvalidOperationInitialization(e.getMessage(), e); + } } } diff --git a/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/OperationFactory.java b/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/OperationFactory.java index 3f84c413c..7cadb2f44 100644 --- a/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/OperationFactory.java +++ b/tooling-cli/src/main/java/org/opencds/cqf/tooling/cli/OperationFactory.java @@ -1,8 +1,11 @@ package org.opencds.cqf.tooling.cli; -//import org.opencds.cqf.tooling.jsonschema.SchemaGenerator; import org.opencds.cqf.tooling.casereporting.transformer.ErsdTransformer; import org.opencds.cqf.tooling.dateroller.DataDateRollerOperation; +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.exception.OperationNotFound; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.OperationParam; import org.opencds.cqf.tooling.terminology.templateToValueSetGenerator.TemplateToValueSetGenerator; import org.apache.commons.lang3.NotImplementedException; import org.opencds.cqf.tooling.Operation; @@ -40,9 +43,92 @@ import org.opencds.cqf.tooling.terminology.VSACBatchValueSetGenerator; import org.opencds.cqf.tooling.terminology.VSACValueSetGenerator; import org.opencds.cqf.tooling.terminology.distributable.DistributableValueSetGenerator; +import org.opencds.cqf.tooling.utilities.OperationUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; class OperationFactory { + private static final Logger logger = LoggerFactory.getLogger(OperationFactory.class); + private static String operationName; + private static Map paramMap; + private static boolean showHelpMenu = false; + + private OperationFactory() { + + } + + private static void processArgs(String[] args) { + paramMap = new HashMap<>(); + for (int i = 1; i < args.length; ++i) { + if (OperationUtils.isHelpArg(args[i])) { + showHelpMenu = true; + return; + } + String[] argAndValue = args[i].split("=", 2); + if (argAndValue.length == 2) { + paramMap.put(argAndValue[0].replace("-", ""), argAndValue[1]); + } + else { + throw new InvalidOperationArgs(String.format( + "Invalid argument: %s found for operation: %s", args[i], operationName)); + } + } + } + + private static ExecutableOperation initialize(ExecutableOperation operation) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + for (Field field : operation.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(OperationParam.class)) { + boolean isInitialized = false; + for (String alias : field.getAnnotation(OperationParam.class).alias()) { + if (paramMap.containsKey(alias)) { + Class paramType = OperationUtils.getParamType(operation, + field.getAnnotation(OperationParam.class).setter()); + operation.getClass().getDeclaredMethod( + field.getAnnotation(OperationParam.class).setter(), paramType + ).invoke(operation, OperationUtils.mapParamType(paramMap.get(alias), paramType)); + isInitialized = true; + } + } + if (!isInitialized) { + if (field.getAnnotation(OperationParam.class).required()) { + throw new InvalidOperationArgs("Missing required argument: " + field.getName()); + } + else if (!field.getAnnotation(OperationParam.class).defaultValue().isEmpty()) { + Class paramType = OperationUtils.getParamType(operation, + field.getAnnotation(OperationParam.class).setter()); + operation.getClass().getDeclaredMethod( + field.getAnnotation(OperationParam.class).setter(), paramType + ).invoke(operation, OperationUtils.mapParamType( + field.getAnnotation(OperationParam.class).defaultValue(), paramType)); + } + } + } + } + return operation; + } + + static ExecutableOperation createOperation(String operationName, Class operationClass, String[] args) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + if (operationClass == null) { + throw new OperationNotFound("Unable to resolve operation: " + operationName); + } + OperationFactory.operationName = operationName; + processArgs(args); + if (showHelpMenu) { + logger.info(OperationUtils.getHelpMenu( + (ExecutableOperation) operationClass.getDeclaredConstructor().newInstance())); + showHelpMenu = false; + return null; + } + return initialize((ExecutableOperation) operationClass.getDeclaredConstructor().newInstance()); + } static Operation createOperation(String operationName) { switch (operationName) { diff --git a/tooling-cli/src/test/java/org/opencds/cqf/tooling/cli/OperationInitializationIT.java b/tooling-cli/src/test/java/org/opencds/cqf/tooling/cli/OperationInitializationIT.java new file mode 100644 index 000000000..e9fd7daf0 --- /dev/null +++ b/tooling-cli/src/test/java/org/opencds/cqf/tooling/cli/OperationInitializationIT.java @@ -0,0 +1,32 @@ +package org.opencds.cqf.tooling.cli; + +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.exception.OperationNotFound; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class OperationInitializationIT { + @Test + void missingOperationName() { + String[] args = new String[]{}; + Assert.assertThrows(OperationNotFound.class, () -> Main.main(args)); + } + + @Test + void invalidOperationName() { + String[] args = new String[]{ "-NonexistentOperationName" }; + Assert.assertThrows(OperationNotFound.class, () -> Main.main(args)); + } + + @Test + void InvalidOperationDeclaration() { + String[] args = new String[]{ "BundleResources", "-ptr=some/directory/path" }; + Assert.assertThrows(InvalidOperationArgs.class, () -> Main.main(args)); + } + + @Test + void missingRequiredOperationArgs() { + String[] args = new String[]{ "-BundleResources" }; + Assert.assertThrows(InvalidOperationArgs.class, () -> Main.main(args)); + } +} diff --git a/tooling/pom.xml b/tooling/pom.xml index a00fbe9c2..c9e47bf6c 100644 --- a/tooling/pom.xml +++ b/tooling/pom.xml @@ -138,6 +138,11 @@ hapi-fhir-base ${hapi.version} + + ca.uhn.hapi.fhir + hapi-fhir-validation + ${hapi.version} + ca.uhn.hapi.fhir @@ -145,6 +150,12 @@ ${hapi.version} + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapi.version} + + com.google.code.gson @@ -158,6 +169,12 @@ commons-lang3 3.12.0 + + com.jakewharton.fliptables + fliptables + 1.1.0 + + @@ -230,14 +247,20 @@ org.slf4j jcl-over-slf4j - 1.7.30 + 1.7.33 org.slf4j log4j-over-slf4j - 1.7.30 + 1.7.33 + + + + org.slf4j + slf4j-jdk14 + 1.7.33 @@ -325,9 +348,20 @@ org.slf4j slf4j-simple - 1.7.30 + 2.0.5 true + + + org.mock-server + mockserver-netty + 5.14.0 + + + org.mock-server + mockserver-client-java + 5.14.0 + diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DTProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DTProcessor.java index 1db48bc44..fadde6379 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DTProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/acceleratorkit/DTProcessor.java @@ -92,12 +92,7 @@ public void execute(String[] args) { private void processWorkbook(Workbook workbook) { String outputPath = getOutputPath(); - try { - ensurePath(outputPath); - } - catch (IOException e) { - throw new IllegalArgumentException(String.format("Could not ensure output path: %s", e.getMessage()), e); - } + ensurePath(outputPath); // process workbook if (decisionTablePages != null) { @@ -614,12 +609,7 @@ private void writeLibraryHeader(StringBuilder cql, Library library) { private void writeLibraries(String outputPath) { if (libraries != null && libraries.size() > 0) { String outputFilePath = outputPath + File.separator + "input" + File.separator + "resources" + File.separator + "library"; - try { - ensurePath(outputFilePath); - } - catch (IOException e) { - throw new IllegalArgumentException(String.format("Could not ensure output path: %s", e.getMessage()), e); - } + ensurePath(outputFilePath); for (Library library : libraries.values()) { writeResource(outputFilePath, library); @@ -632,12 +622,7 @@ private void writeLibraryCQL(String outputPath) { for (Map.Entry entry : libraryCQL.entrySet()) { String outputDirectoryPath = outputPath + File.separator + "input" + File.separator + "cql"; String outputFilePath = outputDirectoryPath + File.separator + entry.getKey() + ".cql"; - try { - ensurePath(outputDirectoryPath); - } - catch (IOException e) { - throw new IllegalArgumentException(String.format("Could not ensure output path: %s", e.getMessage()), e); - } + ensurePath(outputDirectoryPath); try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { writer.write(entry.getValue().toString().getBytes()); @@ -655,12 +640,7 @@ private void writePlanDefinitions(String outputPath) { if (planDefinitions != null && planDefinitions.size() > 0) { for (PlanDefinition planDefinition : planDefinitions.values()) { String outputFilePath = outputPath + File.separator + "input" + File.separator + "resources" + File.separator + "plandefinition"; - try { - ensurePath(outputFilePath); - } - catch (IOException e) { - throw new IllegalArgumentException(String.format("Could not ensure output path: %s", e.getMessage()), e); - } + ensurePath(outputFilePath); writeResource(outputFilePath, planDefinition); } } @@ -702,12 +682,7 @@ private String buildPlanDefinitionIndex() { public void writePlanDefinitionIndex(String outputPath) { String outputFilePath = outputPath + File.separator + "input" + File.separator + "pagecontent"+ File.separator + "PlanDefinitionIndex.md"; - try { - ensurePath(outputFilePath); - } - catch (IOException e) { - throw new IllegalArgumentException(String.format("Could not ensure output path: %s", e.getMessage()), e); - } + ensurePath(outputFilePath); try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { writer.write(buildPlanDefinitionIndex().getBytes()); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/common/BaseCqfmSoftwareSystemHelper.java b/tooling/src/main/java/org/opencds/cqf/tooling/common/BaseCqfmSoftwareSystemHelper.java index 692c1892f..45aca870a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/common/BaseCqfmSoftwareSystemHelper.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/common/BaseCqfmSoftwareSystemHelper.java @@ -51,14 +51,9 @@ protected Boolean getSystemIsValid(CqfmSoftwareSystem system) { } protected void EnsureDevicePath() { - try { - IOUtils.ensurePath(rootDir + devicePath); - if (!IOUtils.resourceDirectories.contains(rootDir + devicePath)) { - IOUtils.resourceDirectories.add(rootDir + devicePath); - } - } - catch (IOException ex) { - LogUtils.putException("EnsureDevicePath", ex.getMessage()); + IOUtils.ensurePath(rootDir + devicePath); + if (!IOUtils.resourceDirectories.contains(rootDir + devicePath)) { + IOUtils.resourceDirectories.add(rootDir + devicePath); } } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/constants/Api.java b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Api.java new file mode 100644 index 000000000..f03985a02 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Api.java @@ -0,0 +1,11 @@ +package org.opencds.cqf.tooling.constants; + +public class Api { + + private Api() {} + + public static final String RXMIX_WORKFLOW_URL = "https://mor.nlm.nih.gov/RxMix/executeConfig.do"; + public static final String LOINC_HIERARCHY_QUERY_URL = "https://loinc.regenstrief.org/searchapi/hierarchy/component-system/search?searchString="; + public static final String LOINC_FHIR_SERVER_URL = "https://fhir.loinc.org"; + public static final String FHIR_LOOKUP_OPERATION_NAME = "$lookup"; +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/constants/Terminology.java b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Terminology.java new file mode 100644 index 000000000..a19eab4b6 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Terminology.java @@ -0,0 +1,21 @@ +package org.opencds.cqf.tooling.constants; + +public class Terminology { + + private Terminology() {} + + public static final String CPG_COMPUTABLE_VS_PROFILE_URL = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-computablevalueset"; + public static final String CPG_EXECUTABLE_VS_PROFILE_URL = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-executablevalueset"; + public static final String RXNORM_SYSTEM_URL = "http://www.nlm.nih.gov/research/umls/rxnorm"; + public static final String LOINC_SYSTEM_URL = "http://loinc.org"; + public static final String RULES_TEXT_EXT_URL = "http://hl7.org/fhir/StructureDefinition/valueset-rules-text"; + public static final String CLINICAL_FOCUS_EXT_URL = "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-clinical-focus"; + public static final String DATA_ELEMENT_SCOPE_EXT_URL = "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-dataelement-scope"; + public static final String VS_INCLUSION_CRITERIA_EXT_URL = "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-inclusion-criteria"; + public static final String VS_EXCLUSION_CRITERIA_EXT_URL = "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-exclusion-criteria"; + public static final String VS_AUTHOR_EXT_URL = "http://hl7.org/fhir/StructureDefinition/valueset-author"; + public static final String KNOWLEDGE_CAPABILITY_EXT_URL = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability"; + public static final String KNOWLEDGE_REPRESENTATION_LEVEL_EXT_URL = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel"; + public static final String USAGE_WARNING_EXT_URL = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-usageWarning"; + public static final String DEFAULT_USAGE_WARNING_VALUE = "This value set contains a point-in-time expansion enumerating the codes that meet the value set intent. As new versions of the code systems used by the value set are released, the contents of this expansion will need to be updated to incorporate newly defined codes that meet the value set intent. Before, and periodically during production use, the value set expansion contents SHOULD be updated. The value set expansion specifies the timestamp when the expansion was produced, SHOULD contain the parameters used for the expansion, and SHALL contain the codes that are obtained by evaluating the value set definition. If this is ONLY an executable value set, a distributable definition of the value set must be obtained to compute the updated expansion."; +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/constants/Validation.java b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Validation.java new file mode 100644 index 000000000..6befc3f50 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/constants/Validation.java @@ -0,0 +1,8 @@ +package org.opencds.cqf.tooling.constants; + +public class Validation { + + private Validation() {} + + public static final String VALIDATION_RESULT_EXTENSION_URL = "http://cms.gov/fhir/mct/StructureDefinition/validation-result"; +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/README.md index 312095d27..0f3f5fbfd 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/README.md +++ b/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/README.md @@ -1,16 +1,24 @@ # RollTestDataDates -This operation takes a file or a directory and rolls forward dates in resources and cds hook requests. It then overwrites the original files with the updated ones. +This operation takes a file or a directory and rolls forward dates in resources and cds hook requests. It then +overwrites the original files with the updated ones. + ## Description of Operation + If a resource in a xml or json file has an extension http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller -and if the current date is greater than the valueDuration set in that extension (i.e. 30 days) that resource will have its date, period, dateTimeType, etc. fields changed according to the relation of the date in that field to the dateLastUpdated value in the extension. This also applies to cds hook request test data. If the extension is not present, that resource is skipped. If the current date is not more than the duration from the lastUpdated date, that resource is skipped. +and if the current date is greater than the valueDuration set in that extension (i.e. 30 days) that resource will have +its date, period, dateTimeType, etc. fields changed according to the relation of the date in that field to the +dateLastUpdated value in the extension. This also applies to cds hook request test data. If the extension is not +present, that resource is skipped. If the current date is not more than the duration from the lastUpdated date, that +resource is skipped. It may be done based on a file name or a directory. An example command line would be: JAVA -jar tooling-cli-2.1.0-SNAPSHOT.jar -RollTestsDataDates -v=r4 -ip="$USER_HOME$/sandbox/rollDate/files/" + OR JAVA -jar tooling-cli-2.1.0-SNAPSHOT.jar -RollTestsDataDates -v=r4 -ip="$USER_HOME$/sandbox/rollDate/files/bundle-example-rec-02-true-make-recommendations.json" diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/ResourceDataDateRoller.java b/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/ResourceDataDateRoller.java index 4a1267e9c..5fb852191 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/ResourceDataDateRoller.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/dateroller/ResourceDataDateRoller.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.List; public class ResourceDataDateRoller { private static Logger logger = LoggerFactory.getLogger(ResourceDataDateRoller.class); @@ -17,16 +18,12 @@ public class ResourceDataDateRoller { public static void rollBundleDates(FhirContext fhirContext, IBaseResource iBaseResource) { switch (fhirContext.getVersion().getVersion().name()) { case "R4": - ArrayList r4ResourceArrayList = BundleUtils.getR4ResourcesFromBundle((org.hl7.fhir.r4.model.Bundle) iBaseResource); - r4ResourceArrayList.forEach(resource -> { - RollDatesR4.rollDatesInResource(resource); - }); + List r4ResourceArrayList = BundleUtils.getR4ResourcesFromBundle((org.hl7.fhir.r4.model.Bundle) iBaseResource); + r4ResourceArrayList.forEach(RollDatesR4::rollDatesInResource); break; case "Stu3": - ArrayList stu3resourceArrayList = BundleUtils.getStu3ResourcesFromBundle((org.hl7.fhir.dstu3.model.Bundle) iBaseResource); - stu3resourceArrayList.forEach(resource -> { - RollDatesDstu3.rollDatesInResource(resource); - }); + List stu3resourceArrayList = BundleUtils.getStu3ResourcesFromBundle((org.hl7.fhir.dstu3.model.Bundle) iBaseResource); + stu3resourceArrayList.forEach(RollDatesDstu3::rollDatesInResource); break; } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidCanonical.java b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidCanonical.java new file mode 100644 index 000000000..a3fec726e --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidCanonical.java @@ -0,0 +1,12 @@ +package org.opencds.cqf.tooling.exception; + +public class InvalidCanonical extends RuntimeException { + static final long serialVersionUID = 1L; + + public InvalidCanonical() { + super(); + } + public InvalidCanonical(String message) { + super(message); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidIdException.java b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidIdException.java new file mode 100644 index 000000000..910f84d96 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidIdException.java @@ -0,0 +1,14 @@ +package org.opencds.cqf.tooling.exception; + +public class InvalidIdException extends RuntimeException { + + static final long serialVersionUID = 1l; + + public InvalidIdException() {super();} + + public InvalidIdException(String message) {super(message);} + + public InvalidIdException(String message, Throwable cause) {super(message, cause);} + + public InvalidIdException(Throwable cause) {super(cause);} +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationArgs.java b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationArgs.java new file mode 100644 index 000000000..48098134c --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationArgs.java @@ -0,0 +1,12 @@ +package org.opencds.cqf.tooling.exception; + +public class InvalidOperationArgs extends RuntimeException { + static final long serialVersionUID = 1L; + + public InvalidOperationArgs() { + super(); + } + public InvalidOperationArgs(String message) { + super(message); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationInitialization.java b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationInitialization.java new file mode 100644 index 000000000..45c0848b8 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/exception/InvalidOperationInitialization.java @@ -0,0 +1,15 @@ +package org.opencds.cqf.tooling.exception; + +public class InvalidOperationInitialization extends RuntimeException { + static final long serialVersionUID = 1L; + + public InvalidOperationInitialization() { + super(); + } + public InvalidOperationInitialization(String message) { + super(message); + } + public InvalidOperationInitialization(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/exception/OperationNotFound.java b/tooling/src/main/java/org/opencds/cqf/tooling/exception/OperationNotFound.java new file mode 100644 index 000000000..ea8680918 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/exception/OperationNotFound.java @@ -0,0 +1,12 @@ +package org.opencds.cqf.tooling.exception; + +public class OperationNotFound extends RuntimeException { + static final long serialVersionUID = 1L; + + public OperationNotFound() { + super(); + } + public OperationNotFound(String message) { + super(message); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/library/LibraryProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/library/LibraryProcessor.java index 11a950e65..cdde81f50 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/library/LibraryProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/library/LibraryProcessor.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; import com.google.common.base.Strings; @@ -22,6 +21,7 @@ import org.opencds.cqf.tooling.library.stu3.STU3LibraryProcessor; import org.opencds.cqf.tooling.parameter.RefreshLibraryParameters; import org.opencds.cqf.tooling.processor.*; +import org.opencds.cqf.tooling.utilities.IDUtils; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; import org.slf4j.Logger; @@ -37,21 +37,6 @@ public class LibraryProcessor extends BaseProcessor { public static String getId(String baseId) { return ResourcePrefix + baseId; } - private static Pattern pattern; - - private static Pattern getPattern() { - if(pattern == null) { - String regex = "^[a-zA-Z]+[a-zA-Z0-9_\\-\\.]*"; - pattern = Pattern.compile(regex); - } - return pattern; - } - - public static void validateIdAlphaNumeric(String id) { - if(!getPattern().matcher(id).find()) { - throw new RuntimeException("The library id format is invalid."); - } - } public List refreshIgLibraryContent(BaseProcessor parentContext, Encoding outputEncoding, Boolean versioned, FhirContext fhirContext, Boolean shouldApplySoftwareSystemStamp) { return refreshIgLibraryContent(parentContext, outputEncoding, null, versioned, fhirContext, shouldApplySoftwareSystemStamp); @@ -216,7 +201,7 @@ private List internalRefreshGeneratedContent(List sourceLibrar newLibrary.setUrl(String.format("%s/Library/%s", (newLibrary.getName().equals("FHIRHelpers") ? "http://hl7.org/fhir" : canonicalBase), fileInfo.getIdentifier().getId())); newLibrary.setId(newLibrary.getName() + (versioned ? "-" + newLibrary.getVersion() : "")); setLibraryType(newLibrary); - validateIdAlphaNumeric(newLibrary.getId()); + IDUtils.validateId(newLibrary.getId(), false); List attachments = new ArrayList(); Attachment attachment = new Attachment(); attachment.setContentType("application/elm+xml"); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/ExecutableOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/ExecutableOperation.java new file mode 100644 index 000000000..eb27338fb --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/ExecutableOperation.java @@ -0,0 +1,5 @@ +package org.opencds.cqf.tooling.operations; + +public interface ExecutableOperation { + void execute(); +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/Operation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/Operation.java new file mode 100644 index 000000000..8280137f4 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/Operation.java @@ -0,0 +1,12 @@ +package org.opencds.cqf.tooling.operations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Operation { + String name(); +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/OperationParam.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/OperationParam.java new file mode 100644 index 000000000..815ea4a73 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/OperationParam.java @@ -0,0 +1,16 @@ +package org.opencds.cqf.tooling.operations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface OperationParam { + String[] alias(); + boolean required() default false; + String setter(); + String defaultValue() default ""; + String description() default ""; +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/README.md new file mode 100644 index 000000000..f935d865d --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/README.md @@ -0,0 +1,68 @@ +# Operations + +This package contains all the operations defined in the CQF Tooling project. Care has been taken to organize the +operations into relevant sub-packages. Operations are defined using the @Operation and @OperationParam annotations to +provide the necessary information and metadata for operation discovery and functionality. Additionally, operations should +implement the appropriate interface for execution (e.g. ExecutableOperation). + +## @Operation Annotation + +The @Operation annotation is a type annotation - meaning it is used at the class level. The annotation is used to tag +operations with the operation name and make it discoverable by the tooling (see the tooling-cli module Main and +OperationFactory classes to see how operations are discovered and initialized). + +### Example + +```java +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.ExecutableOperation; + +@Operation(name = "MyOperation") +class MyOperation implements ExecutableOperation { + @Override + public void execute() { + // Operation logic goes here + } +} +``` + +## @OperationParam Annotation + +The @OperationParam annotation is a field annotation - meaning it is used at the class data member level. The annotation +is used to tag class data members with necessary metadata needed to initialize and validate operation input parameters. +The @OperationParam annotation has the following elements: +- alias - the name(s) used to reference the parameter +- required - determines whether the parameter is required (default is false or not required) +- setter - identifies the method defined in the operation class to set the corresponding parameter value +- defaultValue - defines the default value of the parameter if one is not provided during invocation + +### Example + +```java +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.OperationParam; + +@Operation(name = "MyOperation") +class MyOperation implements ExecutableOperation { + + @OperationParam(alias = {"ptr", "pathtoresources"}, setter = "setPathToResources", required = true) + private String pathToResources; + + @OperationParam(alias = {"e", "encoding"}, setter = "setEncoding", defaultValue = "json") + private String encoding; + + @Override + public void execute() { + // Operation logic goes here + } + + public void setPathToResources(String pathToResources) { + this.pathToResources = pathToResources; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } +} +``` diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessAcceleratorKit.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessAcceleratorKit.java new file mode 100644 index 000000000..ee61ca5e7 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessAcceleratorKit.java @@ -0,0 +1,3287 @@ +package org.opencds.cqf.tooling.operations.acceleratorkit; + +import ca.uhn.fhir.context.FhirContext; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.Type; +import org.hl7.fhir.r4.model.UriType; +import org.hl7.fhir.r4.model.UsageContext; +import org.hl7.fhir.r4.model.ValueSet; +import org.opencds.cqf.tooling.acceleratorkit.CanonicalResourceAtlas; +import org.opencds.cqf.tooling.acceleratorkit.CodeCollection; +import org.opencds.cqf.tooling.acceleratorkit.DictionaryCode; +import org.opencds.cqf.tooling.acceleratorkit.DictionaryElement; +import org.opencds.cqf.tooling.acceleratorkit.DictionaryFhirElementPath; +import org.opencds.cqf.tooling.acceleratorkit.DictionaryProfileElementExtension; +import org.opencds.cqf.tooling.acceleratorkit.ExampleBuilder; +import org.opencds.cqf.tooling.acceleratorkit.InMemoryCanonicalResourceProvider; +import org.opencds.cqf.tooling.acceleratorkit.MultipleChoiceElementChoices; +import org.opencds.cqf.tooling.acceleratorkit.TestCaseProcessor; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.terminology.SpreadsheetHelper; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import static org.opencds.cqf.tooling.utilities.IOUtils.ensurePath; + +@Operation(name = "ProcessAcceleratorKit") +public class ProcessAcceleratorKit implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(ProcessAcceleratorKit.class); + @OperationParam(alias = { "pts", "pathtospreadsheet" }, setter = "setPathToSpreadsheet", required = true) + private String pathToSpreadsheet; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json") + private String encoding; + @OperationParam(alias = { "s", "scopes" }, setter = "setScopes") + private String scopes; + @OperationParam(alias = { "dep", "dataelementpages" }, setter = "setDataElementPages", + description = "comma-separated list of the names of pages in the workbook to be processed") + private String dataElementPages; + @OperationParam(alias = { "tc", "testcases" }, setter = "setTestCaseInput", + description = "path to a spreadsheet containing test case data") + private String testCaseInput; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/acceleratorkit/output") + private String outputPath; + + @OperationParam(alias = {"numid", "numericidallowed"}, setter = "setNumericIdAllowed", defaultValue = "false", + description = "Determines if we want to allow numeric IDs (This overrides default HAPI behaviour") + private String numericIdAllowed; + + private static final String activityCodeSystem = "http://fhir.org/guides/who/anc-cds/CodeSystem/anc-activity-codes"; + private String projectCodeSystemBase; + + private int questionnaireItemLinkIdCounter = 1; + private final String newLine = System.lineSeparator(); + + // Canonical Base + private String canonicalBase = null; + private final Map scopeCanonicalBaseMap = new LinkedHashMap<>(); + + private static final String openMRSSystem = "http://openmrs.org/concepts"; + + // NOTE: for now, disable open MRS system/codes + private static final boolean enableOpenMRS = false; + private final Map supportedCodeSystems = new LinkedHashMap<>(); + private Map elementMap = new LinkedHashMap<>(); + private final Map elementsById = new HashMap<>(); + private final Map activityMap = new LinkedHashMap<>(); + private List profileExtensions = new ArrayList<>(); + private List extensions = new ArrayList<>(); + private List profiles = new ArrayList<>(); + private final Map examples = new HashMap<>(); + private Map> testCases = new LinkedHashMap<>(); + private final Map profilesByElementId = new HashMap<>(); + private final Map> elementsByProfileId = new LinkedHashMap<>(); + private final Map> profilesByActivityId = new LinkedHashMap<>(); + private final Map> profilesByParentProfile = new LinkedHashMap<>(); + private List codeSystems = new ArrayList<>(); + private List questionnaires = new ArrayList<>(); + private List valueSets = new ArrayList<>(); + private final Map valueSetNameMap = new HashMap<>(); + private final Map conceptMaps = new LinkedHashMap<>(); + private final Map concepts = new LinkedHashMap<>(); + private final List retrieves = new ArrayList<>(); + private List igJsonFragments = new ArrayList<>(); + private List igResourceFragments = new ArrayList<>(); + private CanonicalResourceAtlas atlas; + private Row currentInputOptionParentRow; + + private static class RetrieveInfo { + public RetrieveInfo(StructureDefinition structureDefinition, String terminologyIdentifier, DictionaryFhirElementPath fhirElementPath) { + this.structureDefinition = structureDefinition; + this.terminologyIdentifier = terminologyIdentifier; + this.fhirElementPath = fhirElementPath; + } + + private final StructureDefinition structureDefinition; + public StructureDefinition getStructureDefinition() { + return structureDefinition; + } + + private final String terminologyIdentifier; + public String getTerminologyIdentifier() { + return terminologyIdentifier; + } + + private final DictionaryFhirElementPath fhirElementPath; + public DictionaryFhirElementPath getFhirElementPath() { return this.fhirElementPath; } + } + + @Override + public void execute() { + registerScopes(); + registerCodeSystems(); + + Workbook workbook = SpreadsheetHelper.getWorkbook(pathToSpreadsheet); + + if (scopes == null) { + processScope(workbook, null); + } else { + for (String scope : scopes.split(",")) { + processScope(workbook, scope); + } + } + } + + private void registerScopes() { + scopeCanonicalBaseMap.put("core", "http://fhir.org/guides/who/core"); + scopeCanonicalBaseMap.put("anc", "http://fhir.org/guides/who/anc-cds"); + scopeCanonicalBaseMap.put("fp", "http://fhir.org/guides/who/fp-cds"); + scopeCanonicalBaseMap.put("sti", "http://fhir.org/guides/who/sti-cds"); + scopeCanonicalBaseMap.put("cr", "http://fhir.org/guides/cqframework/cr"); + scopeCanonicalBaseMap.put("hiv", "http://fhir.org/guides/nachc/hiv-cds"); + } + + private void registerCodeSystems() { + if (enableOpenMRS) { + supportedCodeSystems.put("OpenMRS", openMRSSystem); + } + supportedCodeSystems.put("ICD-10", "http://hl7.org/fhir/sid/icd-10"); + supportedCodeSystems.put("SNOMED-CT", "http://snomed.info/sct"); + supportedCodeSystems.put("LOINC", "http://loinc.org"); + supportedCodeSystems.put("RxNorm", "http://www.nlm.nih.gov/research/umls/rxnorm"); + supportedCodeSystems.put("CPT", "http://www.ama-assn.org/go/cpt"); + supportedCodeSystems.put("HCPCS", "https://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets"); + + // TODO: Determine and add correct URLS for these Systems + supportedCodeSystems.put("CIEL", "http://hl7.org/fhir/sid/ciel"); + supportedCodeSystems.put("ICD-11", "http://hl7.org/fhir/sid/icd-11"); + supportedCodeSystems.put("ICHI", "https://mitel.dimi.uniud.it/ichi/#http://id.who.int/ichi"); + supportedCodeSystems.put("ICF", "http://hl7.org/fhir/sid/icf-nl"); + supportedCodeSystems.put("NDC", "http://hl7.org/fhir/sid/ndc"); + } + + private void processScope(Workbook workbook, String scope) { + // reset variables + elementMap = new LinkedHashMap<>(); + profileExtensions = new ArrayList<>(); + extensions = new ArrayList<>(); + profiles = new ArrayList<>(); + codeSystems = new ArrayList<>(); + questionnaires = new ArrayList<>(); + valueSets = new ArrayList<>(); + igJsonFragments = new ArrayList<>(); + igResourceFragments = new ArrayList<>(); + + ensurePath(outputPath); + + if (scope != null && scope.length() > 0) { + setCanonicalBase(scopeCanonicalBaseMap.get(scope.toLowerCase())); + } + + // process workbook + for (String page : dataElementPages.split(",")) { + processDataElementPage(workbook, page.trim(), scope); + } + + // process element map + processElementMap(); + + // attached the generated extensions to the profiles that reference them + attachExtensions(); + + // process questionnaires + processQuestionnaires(); + + // process example resources + processExamples(); + + // write all resources + writeExtensions(outputPath); + writeProfiles(outputPath); + writeCodeSystems(outputPath); + writeValueSets(outputPath); + writeConceptMaps(outputPath); + writeQuestionnaires(outputPath); + writeExamples(outputPath); + + processTestCases(); + writeTestCases(outputPath); + + // write concepts CQL + writeConcepts(scope, outputPath); + + // write DataElements CQL + writeDataElements(scope, outputPath); + + //ig.json is deprecated and resources a located by convention. If our output isn't satisfying convention, we should + //modify the tooling to match the convention. + //writeIgJsonFragments(scopePath); + //writeIgResourceFragments(scopePath); + } + + private void processDataElementPage(Workbook workbook, String page, String scope) { + Sheet sheet = workbook.getSheet(page); + if (sheet == null) { + logger.warn("Sheet {} not found in the Workbook, so no processing was done.", page); + return; + } + + questionnaireItemLinkIdCounter = 1; + Questionnaire questionnaire = createQuestionnaireForPage(sheet); + + Iterator it = sheet.rowIterator(); + HashMap colIds = new LinkedHashMap<>(); + String currentGroup = null; + while (it.hasNext()) { + Row row = it.next(); + int headerRow = 1; + // Skip rows prior to header row + if (row.getRowNum() < headerRow) { + continue; + } + // Create column id map + else if (row.getRowNum() == headerRow) { + Iterator colIt = row.cellIterator(); + while (colIt.hasNext()) { + Cell cell = colIt.next(); + String header = SpreadsheetHelper.getCellAsString(cell) + .toLowerCase() + .trim() + .replace("–", "-"); + switch (header) { + case "[anc] data element id": + case "data element id": + colIds.put("DataElementID", cell.getColumnIndex()); + break; + case "[anc] activity id": + case "activity id": + colIds.put("ActivityID", cell.getColumnIndex()); + break; + case "core, fp, sti": + case "scope": + colIds.put("Scope", cell.getColumnIndex()); + break; + case "context": + colIds.put("Context", cell.getColumnIndex()); + break; + case "selector": + colIds.put("Selector", cell.getColumnIndex()); + break; + case "in new dd": + colIds.put("InNewDD", cell.getColumnIndex()); + break; + case "due": + colIds.put("Due", cell.getColumnIndex()); + break; + case "description and definition": + case "description": colIds.put("Description", cell.getColumnIndex()); break; + case "data element label": + colIds.put("DataElementLabel", cell.getColumnIndex()); + colIds.put("Name", cell.getColumnIndex()); + colIds.put("Label", cell.getColumnIndex()); + break; + case "data element name": colIds.put("DataElementName", cell.getColumnIndex()); break; + case "notes": colIds.put("Notes", cell.getColumnIndex()); break; + case "data type": colIds.put("Type", cell.getColumnIndex()); break; + case "multiple choice": + case "multiple choice type": + case "multiple choice (if applicable)": + case "multiple choice type ?(if applicable)": + colIds.put("MultipleChoiceType", cell.getColumnIndex()); break; + case "input options": colIds.put("Choices", cell.getColumnIndex()); break; + case "calculation": colIds.put("Calculation", cell.getColumnIndex()); break; + case "validation required": colIds.put("Constraint", cell.getColumnIndex()); break; + case "required": colIds.put("Required", cell.getColumnIndex()); break; + case "editable": colIds.put("Editable", cell.getColumnIndex()); break; + case "custom profile id": colIds.put("CustomProfileId", cell.getColumnIndex()); break; + case "binding or custom value set name or reference": colIds.put("CustomValueSetName", cell.getColumnIndex()); break; + case "binding strength": colIds.put("BindingStrength", cell.getColumnIndex()); break; + case "ucum": colIds.put("UnitOfMeasure", cell.getColumnIndex()); break; + case "extension needed": colIds.put("ExtensionNeeded", cell.getColumnIndex()); break; + // fhir resource details + case "master data element path": colIds.put("MasterDataElementPath", cell.getColumnIndex()); break; + case "hl7 fhir r4 - resource": colIds.put("FhirR4Resource", cell.getColumnIndex()); break; + case "hl7 fhir r4 - resource type": colIds.put("FhirR4ResourceType", cell.getColumnIndex()); break; + case "hl7 fhir r4 - base profile": colIds.put("FhirR4BaseProfile", cell.getColumnIndex()); break; + case "hl7 fhir r4 - version number": colIds.put("FhirR4VersionNumber", cell.getColumnIndex()); break; + case "hl7 fhir r4 - additional fhir mapping details": colIds.put("FhirR4AdditionalFHIRMappingDetails", cell.getColumnIndex()); break; + // terminology + case "fhir code system": colIds.put("FhirCodeSystem", cell.getColumnIndex()); break; + case "hl7 fhir r4 code": colIds.put("FhirR4Code", cell.getColumnIndex()); break; + case "hl7 fhir r4 code display": colIds.put("FhirR4CodeDisplay", cell.getColumnIndex()); break; + case "hl7 fhir r4 code definition": colIds.put("FhirR4CodeDefinition", cell.getColumnIndex()); break; + case "icd-10-who": + case "icd-10 code": + case "icd-10?code": colIds.put("ICD-10", cell.getColumnIndex()); break; + case "icd-10?comments / considerations": colIds.put("ICD-10Comments", cell.getColumnIndex()); break; + case "icf?code": colIds.put("ICF", cell.getColumnIndex()); break; + case "icf?comments / considerations": colIds.put("ICFComments", cell.getColumnIndex()); break; + case "ichi?code": + case "ichi (beta 3)?code": colIds.put("ICHI", cell.getColumnIndex()); break; + case "ichi?comments / considerations": colIds.put("ICHIComments", cell.getColumnIndex()); break; + case "snomed-ct": + case "snomed-ct code": + case "snomed ct": + case "snomed ct?code": + case "snomed ct international version?code": colIds.put("SNOMED-CT", cell.getColumnIndex()); break; + case "snomed ct international version?comments / considerations": colIds.put("SNOMEDComments", cell.getColumnIndex()); break; + case "loinc": + case "loinc code": + case "loinc version 2.68?code": colIds.put("LOINC", cell.getColumnIndex()); break; + case "loinc version 2.68?comments / considerations": colIds.put("LOINCComments", cell.getColumnIndex()); break; + case "rxnorm": + case "rxnorm code": + case "rxnorm?code": colIds.put("RxNorm", cell.getColumnIndex()); break; + case "rxnorm?comments / considerations": colIds.put("RXNormComments", cell.getColumnIndex()); break; + case "icd-11": + case "icd-11 code": + case "icd-11?code":colIds.put("ICD-11", cell.getColumnIndex()); break; + case "icd-11?comments / considerations": colIds.put("ICD-11Comments", cell.getColumnIndex()); break; + case "ciel": colIds.put("CIEL", cell.getColumnIndex()); break; + case "openmrs entity parent": colIds.put("OpenMRSEntityParent", cell.getColumnIndex()); break; + case "openmrs entity": colIds.put("OpenMRSEntity", cell.getColumnIndex()); break; + case "openmrs entity id": colIds.put("OpenMRSEntityId", cell.getColumnIndex()); break; + case "cpt": + case "cpt code": + case "cpt?code": colIds.put("CPT", cell.getColumnIndex()); break; + case "cpt?comments / considerations": colIds.put("CPTComments", cell.getColumnIndex()); break; + case "hcpcs": + case "hcpcs code": + case "hcpcs?code": + case "hcpcs level ii code": + case "hcpcs?level ii code": colIds.put("HCPCS", cell.getColumnIndex()); break; + case "hcpcs?comments / considerations": colIds.put("HCPCSComments", cell.getColumnIndex()); break; + case "ndc": + case "ndc code": + case "ndc?code": colIds.put("NDC", cell.getColumnIndex()); break; + case "ndc?comments / considerations": colIds.put("NDCComments", cell.getColumnIndex()); break; + } + } + continue; + } + + String rowScope = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Scope")); + boolean scopeIsNull = scope == null; + boolean scopeMatchesRowScope = rowScope != null && scope.equalsIgnoreCase(rowScope.toLowerCase()); + + String inNewDD = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "InNewDD")); + boolean shouldInclude = inNewDD == null || inNewDD.equals("ST") || inNewDD.equals("1"); + + if (shouldInclude && (scopeIsNull || scopeMatchesRowScope)) { + String masterDataType = getMasterDataType(row, colIds); + if (masterDataType != null) { + switch (masterDataType) { + case "Data Element": + case "Slice": + currentInputOptionParentRow = row; + DictionaryElement e = createDataElement(page, currentGroup, row, colIds); + if (e != null) { + elementMap.put(e.getName(), e); + elementsById.put(e.getId(), e); + updateQuestionnaireForDataElement(e, questionnaire); + } + break; + case "Input Option": + addInputOptionToParentElement(row, colIds); + break; + case "Calculation": + case "UI Element": + break; + default: + // Currently unsupported/undocumented + break; + } + } + } + } + questionnaires.add(questionnaire); + } + + private void processElementMap() { + for (DictionaryElement element : elementMap.values()) { + if (requiresProfile(element)) { + ensureProfile(element); + } + } + } + + private void attachExtensions() { + // Add extensions to the appropriate profiles + for (DictionaryProfileElementExtension profileElementExtension : profileExtensions) { + for (StructureDefinition profile : profiles) { + if (profile.getId().equals(profileElementExtension.getProfileId())) { + StructureDefinition extensionDefinition = profileElementExtension.getExtension(); + + String extensionName = getExtensionName(profileElementExtension.getResourcePath(), + profile.getName()); + + ElementDefinition extensionBaseElement = getDifferentialElement(extensionDefinition, "Extension.extension"); + + String resourcePath = profileElementExtension.getResourcePath(); + String pathToElementBeingExtended = resourcePath.substring(0, + resourcePath.indexOf(extensionName) - 1); + String extensionId = pathToElementBeingExtended + ".extension:" + extensionName; + + ElementDefinition extensionElement = new ElementDefinition(); + extensionElement.setId(extensionId); + extensionElement.setPath(pathToElementBeingExtended + ".extension"); + extensionElement.setSliceName(extensionName); + extensionElement.setMin(extensionBaseElement.getMin()); + extensionElement.setMax(extensionBaseElement.getMax()); + + ElementDefinition.TypeRefComponent typeRefComponent = new ElementDefinition.TypeRefComponent(); + List typeProfileList = new ArrayList<>(); + typeProfileList.add(new CanonicalType(extensionDefinition.getUrl())); + typeRefComponent.setProfile(typeProfileList); + typeRefComponent.setCode("Extension"); + + List typeRefList = new ArrayList<>(); + typeRefList.add(typeRefComponent); + + extensionElement.setType(typeRefList); + + profile.getDifferential().addElement(extensionElement); + applyDataElementToElementDefinition(profileElementExtension.getElement(), profile, extensionElement); + } + } + } + } + + 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().getResourceTypeAndPath())); + } + else { + item.setDefinition(sd.getUrl()); + } + } + else { + item.setDefinition(null); + } + } + else { + item.setDefinition(null); + } + } + } + } + } + + // Generate example resources for each profile + public void processExamples() { + ExampleBuilder eb = new ExampleBuilder(); + eb.setAtlas(getAtlas()); + eb.setPatientContext("anc-patient-example"); + eb.setEncounterContext("anc-encounter-example"); + eb.setLocationContext("anc-location-example"); + eb.setPractitionerContext("anc-practitioner-example"); + eb.setPractitionerRoleContext("anc-practitionerrole-example"); + for (StructureDefinition sd : profiles) { + examples.put(sd.getUrl(), eb.build(sd)); + } + } + + public void writeExtensions(String scopePath) { + if (extensions != null && !extensions.isEmpty()) { + String extensionsPath = getExtensionsPath(scopePath); + ensureExtensionsPath(scopePath); + + for (StructureDefinition sd : extensions) { + writeResource(extensionsPath, sd); + + /* Generate JSON fragment for inclusion in the IG: + "StructureDefinition/": { + "source": "structuredefinition/structuredefinition-.json", + "defns": "StructureDefinition--definitions.html", + "base": "StructureDefinition-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"StructureDefinition/%s\": {\r%n\t\t\t\"source\": \"structuredefinition/structuredefinition-%s.json\",\r%n\t\t\t\"defns\": \"StructureDefinition-%s-definitions.html\",\r%n\t\t\t\"base\": \"StructureDefinition-%s.html\"\r%n\t\t}", + sd.getId(), sd.getId(), sd.getId(), sd.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", sd.getId())); + } + } + } + + public void writeProfiles(String scopePath) { + if (profiles != null && !profiles.isEmpty()) { + String profilesPath = getProfilesPath(scopePath); + ensureProfilesPath(scopePath); + + for (StructureDefinition sd : profiles) { + indexProfile(sd); + writeResource(profilesPath, sd); + + /* Generate JSON fragment for inclusion in the IG: + "StructureDefinition/": { + "source": "structuredefinition/structuredefinition-.json", + "defns": "StructureDefinition--definitions.html", + "base": "StructureDefinition-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"StructureDefinition/%s\": {\r%n\t\t\t\"source\": \"structuredefinition/structuredefinition-%s.json\",\r%n\t\t\t\"defns\": \"StructureDefinition-%s-definitions.html\",\r%n\t\t\t\"base\": \"StructureDefinition-%s.html\"\r%n\t\t}", + sd.getId(), sd.getId(), sd.getId(), sd.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", sd.getId())); + } + } + } + + public void writeCodeSystems(String scopePath) { + if (codeSystems != null && codeSystems.size() > 0) { + String codeSystemPath = getCodeSystemPath(scopePath); + ensureCodeSystemPath(scopePath); + + for (CodeSystem cs : codeSystems) { + writeResource(codeSystemPath, cs); + + /* Generate JSON fragment for inclusion in the IG: + "CodeSystem/": { + "source": "codesystem/codesystem-.json", + "base": "CodeSystem-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"CodeSystem/%s\": {\r%n\t\t\t\"source\": \"codesystem/codesystem-%s.json\",\r%n\t\t\t\"base\": \"CodeSystem-%s.html\"\r%n\t\t}", + cs.getId(), cs.getId(), cs.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", cs.getId())); + } + } + } + + public void writeValueSets(String scopePath) { + if (valueSets != null && !valueSets.isEmpty()) { + String valueSetPath = getValueSetPath(scopePath); + ensureValueSetPath(scopePath); + + for (ValueSet vs : valueSets) { + writeResource(valueSetPath, vs); + + /* Generate JSON fragment for inclusion in the IG: + "ValueSet/": { + "source": "valueset/valueset-.json", + "base": "ValueSet-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"ValueSet/%s\": {\r%n\t\t\t\"source\": \"valueset/valueset-%s.json\",\r%n\t\t\t\"base\": \"ValueSet-%s.html\"\r%n\t\t}", + vs.getId(), vs.getId(), vs.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", vs.getId())); + } + } + } + + public void writeConceptMaps(String scopePath) { + if (conceptMaps.size() > 0) { + String conceptMapPath = getConceptMapPath(scopePath); + ensureConceptMapPath(scopePath); + + for (ConceptMap cm : conceptMaps.values()) { + writeResource(conceptMapPath, cm); + + /* Generate JSON fragment for inclusion in the IG: + "ConceptMap/": { + "source": "conceptmap/conceptmap-.json", + "base": "ConceptMap-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"ConceptMap/%s\": {\r%n\t\t\t\"source\": \"conceptmap/conceptmap-%s.json\",\r%n\t\t\t\"base\": \"ConceptMap-%s.html\"\r%n\t\t}", + cm.getId(), cm.getId(), cm.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", cm.getId())); + } + } + } + + public void writeQuestionnaires(String scopePath) { + if (questionnaires != null && !questionnaires.isEmpty()) { + String questionnairePath = getQuestionnairePath(scopePath); + ensureQuestionnairePath(scopePath); + + for (Questionnaire q : questionnaires) { + writeResource(questionnairePath, q); + + /* Generate JSON fragment for inclusion in the IG: + "Questionnaire/": { + "source": "questionnaire/questionnaire-.json", + "base": "Questionnaire-.html" + } + */ + igJsonFragments.add(String.format("\t\t\"Questionnaire/%s\": {\r%n\t\t\t\"source\": \"questionnaire/questionnaire-%s.json\",\r%n\t\t\t\"base\": \"Questionnaire-%s.html\"\r%n\t\t}", + q.getId(), q.getId(), q.getId())); + + /* Generate XML fragment for the IG resource: + + + + + + + */ + igResourceFragments.add(String.format("\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t\t\r%n\t\t\t", q.getId())); + } + } + } + + public void writeExamples(String scopePath) { + if (examples.size() > 0) { + String examplesPath = getExamplesPath(scopePath); + ensureExamplesPath(scopePath); + for (Map.Entry entry : examples.entrySet()) { + writeResource(examplesPath, entry.getValue()); + } + } + } + + public void processTestCases() { + if (testCaseInput != null && !testCaseInput.isEmpty()) { + TestCaseProcessor tcp = new TestCaseProcessor(); + tcp.setAtlas(getAtlas()); + tcp.setProfilesByElementId(profilesByElementId); + testCases = tcp.process(testCaseInput); + } + } + + public void writeTestCases(String scopePath) { + if (testCases != null && testCases.size() > 0) { + String testsPath = getTestsPath(scopePath); + ensureTestsPath(scopePath); + for (Map.Entry> entry : testCases.entrySet()) { + String testPath = getTestPath(scopePath, entry.getKey()); + ensureTestPath(scopePath, entry.getKey()); + for (Resource r : entry.getValue()) { + writeResource(testPath, r); + } + } + } + } + + public void writeConcepts(String scope, String scopePath) { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("library %sConcepts", scope)); + sb.append(newLine); + sb.append(newLine); + + sb.append("// Code Systems"); + sb.append(newLine); + // Supported code systems + for (Map.Entry entry : supportedCodeSystems.entrySet()) { + sb.append(String.format("codesystem \"%s\": '%s'", entry.getKey(), entry.getValue())); + sb.append(newLine); + } + // For each code system, generate a codesystem CQL entry: + // codesystem "CodeSystem.title": 'CodeSystem.url' [version 'CodeSystem.version'] + for (CodeSystem cs : codeSystems) { + String identifier = getCodeSystemIdentifier(cs); + sb.append(String.format("codesystem \"%s\": '%s'", identifier, cs.getUrl())); + if (cs.hasVersion()) { + sb.append(String.format(" version '%s'", cs.getVersion())); + } + sb.append(newLine); + } + + sb.append(newLine); + sb.append("// Value Sets"); + sb.append(newLine); + // For each value set generate a valueset CQL entry: + // valueset "ValueSet.title": 'ValueSet.url' [version 'ValueSet.version'] + for (ValueSet vs : valueSets) { + sb.append(String.format("valueset \"%s\": '%s'", vs.hasTitle() ? vs.getTitle() : vs.getName(), vs.getUrl())); + if (vs.hasVersion()) { + sb.append(String.format(" version '%s'", vs.getVersion())); + } + sb.append(newLine); + } + + sb.append(newLine); + sb.append("// Codes"); + sb.append(newLine); + // For each concept generate a code entry: + // code "ConceptName": 'Coding.value' from 'getCodeSystemName(Coding.system)' display 'Coding.display' + for (Map.Entry entry : concepts.entrySet()) { + sb.append(String.format("code \"%s\": '%s' from \"%s\"", entry.getKey(), entry.getValue().getCode(), getCodeSystemIdentifier(entry.getValue()))); + if (entry.getValue().hasDisplay()) { + sb.append(String.format(" display '%s'", entry.getValue().getDisplay())); + } + sb.append(newLine); + } + + ensureCqlPath(scopePath); + try (FileOutputStream writer = new FileOutputStream(getCqlPath(scopePath) + "/" + scope + "Concepts.cql")) { + writer.write(sb.toString().getBytes()); + writer.flush(); + } + catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing concepts library source"); + } + } + + public void writeDataElements(String scope, String scopePath) { + writeDataElements(scope, scopePath, "Patient"); + writeDataElements(scope, scopePath, "Encounter"); + } + + public void writeDataElements(String scope, String scopePath, String context) { + StringBuilder sb = new StringBuilder(); + StringBuilder activityIndex = new StringBuilder(); + + sb.append(String.format("library %s%sDataElements", scope, context.equals("Encounter") ? "Contact" : "")); + sb.append(newLine).append(newLine); + + sb.append("using FHIR version '4.0.1'"); + sb.append(newLine).append(newLine); + sb.append("include FHIRHelpers version '4.0.1'"); + sb.append(newLine); + sb.append("include FHIRCommon called FC"); + sb.append(newLine).append(newLine); + + sb.append("include WHOCommon called WC"); + sb.append(newLine); + sb.append(String.format("include %sCommon called AC", scope)); + sb.append(newLine); + sb.append(String.format("include %sConcepts called Cx", scope)); + sb.append(newLine).append(newLine); + + sb.append(String.format("context %s", context)); + sb.append(newLine).append(newLine); + + // For each StructureDefinition, generate an Expression Definition: + /* + // @dataElement: StructureDefinition.identifier + // @activity: StructureDefinition.useContext[task] + // @description: StructureDefinition.description + define "StructureDefinition.title": + [StructureDefinition.resourceType: terminologyIdentifier] + */ + + List activityIds = new ArrayList(profilesByActivityId.keySet()); + activityIds.sort(activityIdComparator); + for (String activityId : activityIds) { + writeActivityIndexHeader(activityIndex, activityId); + + List sds = profilesByActivityId.get(activityId); + sds.sort(Comparator.comparing(StructureDefinition::getId)); + for (StructureDefinition sd : sds) { + writeDataElement(sb, sd, context); + writeActivityIndexEntry(activityIndex, sd); + } + } + + ensureCqlPath(scopePath); + try (FileOutputStream writer = new FileOutputStream(getCqlPath(scopePath) + "/" + scope + (context.equals("Encounter") ? "Contact" : "") + "DataElements.cql")) { + writer.write(sb.toString().getBytes()); + writer.flush(); + } + catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing concepts library source"); + } + + try (FileOutputStream writer = new FileOutputStream(getCqlPath(scopePath) + "/" + scope + "DataElementsByActivity.md")) { + writer.write(activityIndex.toString().getBytes()); + writer.flush(); + } + catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing profile activity index"); + } + } + + private void writeDataElement(StringBuilder sb, StructureDefinition sd, String context) { + // TODO: Consider writing this to an extension on the structuredefinition instead of to the retrieveInfo like this + //for (RetrieveInfo retrieve : retrieves) { + // if (retrieve.structureDefinition.getId().equals(sd.getId())) { + // BTR -> Switched to drive off the data elements mapped into this profile + List lde = elementsByProfileId.get(sd.getId()); + if (lde != null) { + for (DictionaryElement de : lde) { + //String title = sd.hasTitle() ? sd.getTitle() : sd.hasName() ? sd.getName() : sd.getId(); + String title = de.getDataElementLabel(); + sb.append("/*"); + sb.append(newLine); + sb.append(" @dataElement: "); + sb.append(String.format("%s ", de.getId())); + //Identifier dataElementIdentifier = getDataElementIdentifier(sd.getIdentifier()); + //if (dataElementIdentifier != null) { + // sb.append(String.format("%s ", dataElementIdentifier.getValue())); + //} + //sb.append(title); + sb.append(de.getDataElementLabel()); + sb.append(newLine); + + Coding activityCoding = getActivityCoding(sd); + if (activityCoding != null) { + sb.append(String.format(" @activity: %s %s", activityCoding.getCode(), activityCoding.getDisplay())); + sb.append(newLine); + } + + if (sd.hasDescription()) { + //sb.append(String.format(" @description: %s", sd.getDescription())); + sb.append(String.format(" @description: %s", de.getDescription())); + sb.append(newLine); + } + sb.append("*/"); + sb.append(newLine); + sb.append(String.format("define \"%s\":", title)); + sb.append(newLine); + // If we are generating for the context specified for the data element, and there is a selector, use it + boolean inContext = (de.getContext() != null && de.getContext().equals(context)) + || (de.getContext() == null && context.equals("Patient")); + boolean useSelector = inContext && de.getSelector() != null; + if (useSelector) { + sb.append(String.format(" WC.%s(", de.getSelector())); + sb.append(newLine); + } + if (de.getTerminologyIdentifier() != null && !de.getTerminologyIdentifier().isEmpty()) { + sb.append(String.format(" [%s: Cx.\"%s\"]", sd.getType(), de.getTerminologyIdentifier())); + } + else { + sb.append(String.format(" [%s]", sd.getType())); + } + + DictionaryFhirElementPath fhirElementPath = de.getFhirElementPath(); + + // TODO: Switch on sd.baseDefinition to provide filtering here (e.g. status = 'not-done') + String alias; + switch (sd.getBaseDefinition()) { + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-base-patient": + alias = "P"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-base-encounter": + alias = "E"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-condition": + alias = "C"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.clinicalStatus in FC.\"Active Condition\"", alias)); + sb.append(newLine); + sb.append(String.format(" and %s.verificationStatus ~ FC.\"confirmed\"", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + // TODO: Should this contextualize to encounter? + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-immunization": + alias = "I"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status = 'completed'", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-immunizationnotdone": + alias = "IND"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status = 'not-done'", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-medicationrequest": + alias = "MR"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(" where MR.status in { 'draft', 'active', 'on-hold', 'completed' }"); + sb.append(newLine); + sb.append(" and Coalesce(MR.doNotPerform, false) is false"); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-medicationnotrequested": + alias = "MR"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(" where MR.status in { 'draft', 'active', 'on-hold', 'completed' }"); + sb.append(newLine); + sb.append(" and MR.doNotPerform is true"); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-observation": + alias = "O"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status in { 'final', 'amended', 'corrected' }", alias)); + sb.append(newLine); + sb.append(String.format(" and Coalesce(WC.ModifierExtension(%s, 'who-notDone').value, false) is false", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-observationnotdone": + alias = "OND"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where WC.ModifierExtension(%s, 'who-notDone').value is true", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-procedure": + alias = "P"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status in { 'preparation', 'in-progress', 'on-hold', 'completed' }", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-procedurenotdone": + alias = "PND"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status = 'not-done'", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-servicerequest": + alias = "SR"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status in { 'draft', 'active', 'on-hold', 'completed' }", alias)); + sb.append(newLine); + sb.append(String.format(" and Coalesce(%s.doNotPerform, false) is false", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + case "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-servicenotrequested": + alias = "SNR"; + sb.append(String.format(" %s", alias)); + sb.append(newLine); + sb.append(String.format(" where %s.status in { 'draft', 'active', 'on-hold', 'completed' }", alias)); + sb.append(newLine); + sb.append(String.format(" and %s.doNotPerform is true", alias)); + sb.append(newLine); + if (context.equals("Encounter")) { + sb.append(String.format(" and Last(Split(%s.encounter.reference, '/')) = Encounter.id", alias)); + sb.append(newLine); + } + appendReturnClause(sb, fhirElementPath, alias, inContext, useSelector); + break; + default: + break; + } + sb.append(newLine); + sb.append(newLine); + } + } + } + + private void appendReturnClause(StringBuilder sb, DictionaryFhirElementPath fhirElementPath, String alias, boolean inContext, boolean useSelector) { + if (useSelector) { + sb.append(" )"); + } + //TODO: If an extension, append the extension-specific return clause + // P.extension E where E.url = 'http://fhir.org/guides/who-int/anc-cds/StructureDefinition/occupation' return E.value as CodeableConcept + if (fhirElementPath != null) { + String returnElementPath = fhirElementPath.getResourcePath(); + String cast = ""; + if (isChoiceType(fhirElementPath)) { + returnElementPath = fhirElementPath.getResourcePath().replace("[x]", ""); + cast = String.format(" as FHIR.%s", fhirElementPath.getFhirElementType()); + } + + if (useSelector) { + sb.append(String.format(".%s%s", returnElementPath, cast)); + sb.append(newLine); + } + else if (inContext) { + sb.append(String.format(" return %s.%s%s", alias, returnElementPath, cast)); + sb.append(newLine); + } + + } + } + + private Questionnaire createQuestionnaireForPage(Sheet sheet) { + Questionnaire questionnaire = new Questionnaire(); + Coding activityCoding = getActivityCoding(sheet.getSheetName()); + questionnaire.setId(IDUtils.toUpperId(activityCoding.getCode(), isNumericIdAllowed())); + + questionnaire.getExtension().add( + new Extension("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability", new CodeType("shareable"))); + questionnaire.getExtension().add( + new Extension("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability", new CodeType("computable"))); + questionnaire.getExtension().add( + new Extension("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability", new CodeType("publishable"))); + questionnaire.getExtension().add( + new Extension("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel", new CodeType("structured"))); + + questionnaire.setUrl(String.format("%s/Questionnaire/%s", canonicalBase, questionnaire.getId())); + questionnaire.setName(questionnaire.getId()); + questionnaire.setTitle(sheet.getSheetName()); + questionnaire.setStatus(Enumerations.PublicationStatus.ACTIVE); + questionnaire.setExperimental(false); + 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(activityCoding.getSystem(), activityCoding.getCode(), activityCoding.getDisplay())); + UsageContext useContext = new UsageContext(useContextCoding, useContextValue); + questionnaire.getUseContext().add(useContext); + + return questionnaire; + } + + private int getColId(HashMap colIds, String colName) { + if (colIds.containsKey(colName)) { + return colIds.get(colName); + } + + return -1; + } + + private String getMasterDataType(Row row, HashMap colIds) { + String masterDataType = null; + String activityID = getActivityID(row, colIds); + if (activityID != null && !activityID.isEmpty()) { + String multipleChoiceType = getMultipleChoiceType(row, colIds); + masterDataType = multipleChoiceType != null && multipleChoiceType.equalsIgnoreCase("Input Option") ? "Input Option" : "Data Element"; + } + + return masterDataType; + } + + private DictionaryElement createDataElement(String page, String group, Row row, HashMap colIds) { + String type = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Type")); + if (type != null) { + type = type.trim(); + if (type.equals("Coding")) { + String choiceType = getMultipleChoiceType(row, colIds); + if (choiceType != null) { + choiceType = choiceType.trim(); + type = type + " - " + choiceType; + } + } + } + String name = getName(row, colIds); + if (name.isEmpty()) { + return null; + } + name = name.trim(); + String label = name; + + // TODO: should we throw if a duplicate is found within the same scope? + // TODO: (core, anc, sti, fp, etc) + if (elementMap.containsKey(name)) { + // throw new IllegalArgumentException("Duplicate Name encountered: " + name); + return null; + } + + String activity = getActivityID(row, colIds); + Coding activityCoding = getActivityCoding(activity); + //String id = getNextElementId(activityCoding.getCode()); + String id = getDataElementID(row, colIds); + + DictionaryElement e = new DictionaryElement(id, name); + + // Populate based on the row + e.setPage(page); + e.setGroup(group); + e.setActivity(activity); + e.setLabel(label); + e.setType(type); + e.setMasterDataType(getMasterDataType(row, colIds)); + e.setInfoIcon(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "InfoIcon"))); + e.setDue(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Due"))); + e.setRelevance(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Relevance"))); + e.setDescription(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Description"))); + e.setDataElementLabel(getDataElementLabel(row, colIds) != null ? getDataElementLabel(row, colIds).trim() : null); + e.setDataElementName(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "DataElementName"))); + e.setNotes(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Notes"))); + e.setCalculation(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Calculation"))); + e.setConstraint(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Constraint"))); + e.setRequired(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Required"))); + e.setEditable(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Editable"))); + e.setScope(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Scope"))); + 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(id, name, row, colIds)); + + DictionaryFhirElementPath fhirElementPath = getFhirElementPath(row, colIds); + if (fhirElementPath != null) { + e.setFhirElementPath(fhirElementPath); + } + e.setMasterDataElementPath(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "MasterDataElementPath"))); + e.setBaseProfile(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4BaseProfile"))); + e.setCustomProfileId(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "CustomProfileId"))); + e.setVersion(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4VersionNumber"))); + e.setCustomValueSetName(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "CustomValueSetName"))); + e.setBindingStrength(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "BindingStrength"))); + e.setUnitOfMeasure(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "UnitOfMeasure"))); + e.setExtensionNeeded(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "ExtensionNeeded"))); + + e.setAdditionalFHIRMappingDetails(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4AdditionalFHIRMappingDetails"))); + + return e; + } + + private void updateQuestionnaireForDataElement(DictionaryElement dataElement, Questionnaire questionnaire) { + Questionnaire.QuestionnaireItemComponent questionnaireItem = new Questionnaire.QuestionnaireItemComponent(); + questionnaireItem.setLinkId(String.valueOf(questionnaireItemLinkIdCounter)); + String definition = dataElement.getId(); + questionnaireItem.setDefinition(definition); + questionnaireItem.setText(dataElement.getDataElementLabel()); + Questionnaire.QuestionnaireItemType questionnaireItemType = getQuestionnaireItemType(dataElement); + if (questionnaireItemType != null) { + questionnaireItem.setType(questionnaireItemType); + } else { + logger.warn("Unable to determine questionnaire item type for item '{}'.", dataElement.getDataElementLabel()); + } + + questionnaire.getItem().add(questionnaireItem); + + questionnaireItemLinkIdCounter = questionnaireItemLinkIdCounter + 1; + } + + private void addInputOptionToParentElement(Row row, HashMap colIds) { + String parentId = getDataElementID(currentInputOptionParentRow, colIds).trim(); + String parentName = getDataElementLabel(currentInputOptionParentRow, colIds).trim(); + + if (!parentId.isEmpty() || !parentName.isEmpty()) + { + DictionaryElement parentElement = elementMap.get(parentName); + if (parentElement != null) { + // The choices FHIR Element Path is set by the first "Input Option" row in the group and will NOT be + // overridden, if set, by subsequent input option rows. + DictionaryFhirElementPath parentChoicesFhirElementPath = parentElement.getChoices().getFhirElementPath(); + if (parentChoicesFhirElementPath == null) { + DictionaryFhirElementPath parentElementFhirElementPath = parentElement.getFhirElementPath(); + parentChoicesFhirElementPath = getFhirElementPath(row, colIds); + + if (parentChoicesFhirElementPath == null + && parentElementFhirElementPath != null + && parentElementFhirElementPath.getResourceTypeAndPath().equals("Observation.value[x]")) { + parentChoicesFhirElementPath = parentElementFhirElementPath; + } + + parentElement.getChoices().setFhirElementPath(parentChoicesFhirElementPath); + } + + Map> valueSetCodes = parentElement.getChoices().getValueSetCodes(); + if (valueSetCodes != null) { + String inputOptionValueSetName = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "CustomValueSetName")); + if (inputOptionValueSetName == null || inputOptionValueSetName.isEmpty()) { + inputOptionValueSetName = parentName; + } + + if (!inputOptionValueSetName.endsWith("Choices")) { + inputOptionValueSetName = inputOptionValueSetName + " Choices"; + } + + String optionId = getDataElementID(row, colIds); + String optionLabel = getDataElementLabel(row, colIds); + 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); + for (DictionaryCode code: inputOptionCodes) { + if (entryCodes.stream().noneMatch(c -> c.getCode().equals(code.getCode()) + && c.getSystem().equals(code.getSystem()))) { + entryCodes.add(code); + } + + } + } else { + valueSetCodes.put(inputOptionValueSetName, inputOptionCodes); + } + } + } + } + } + + private boolean requiresProfile(DictionaryElement element) { + if (element == null + || element.getMasterDataType() == null + || element.getFhirElementPath() == null) { + return false; + } + + switch (element.getMasterDataType().toLowerCase().trim()) { + case "data element": + case "input option": + return true; + case "calculation": + case "slice": + // TODO: Do we need to do anything with these? + return true; + case "ui element": + return false; + default: + return false; + } + } + + private void ensureProfile(DictionaryElement element) { + StructureDefinition sd = null; + + // If custom profile is specified, search for if it exists already. + String customProfileIdRaw = element.getCustomProfileId(); + String profileId = IDUtils.toId(customProfileIdRaw != null && !customProfileIdRaw.isEmpty() ? + customProfileIdRaw : element.getId(), isNumericIdAllowed()); + for (StructureDefinition profile : profiles) { + if (profile.getId().equals(profileId)) { + sd = profile; + } + } + + // If the profile doesn't exist, create it with the root element. + if (sd == null) { + sd = createProfileStructureDefinition(element, profileId); + } + + if (requiresExtension(element)) { + StructureDefinition extension = ensureExtension(element); + DictionaryProfileElementExtension profileElementExtensionEntry = new DictionaryProfileElementExtension(); + profileElementExtensionEntry.setProfileId(profileId); + profileElementExtensionEntry.setResourcePath(element.getFhirElementPath().getResourceTypeAndPath()); + profileElementExtensionEntry.setElement(element); + profileElementExtensionEntry.setExtension(extension); + profileExtensions.add(profileElementExtensionEntry); + } + else { + // Ensure that the element is added to the StructureDefinition + ensureElement(element, sd); + } + + if (!profiles.contains(sd)) { + profiles.add(sd); + } + } + + private String getExtensionName(String resourcePath, String dataElementName) { + String extensionName = null; + String[] resourcePathComponents = resourcePath.split("\\."); + if (resourcePathComponents.length == 1) { + extensionName = resourcePathComponents[0]; + } else if (resourcePathComponents.length > 1) { + extensionName = resourcePathComponents[resourcePathComponents.length - 1]; + } else { + extensionName = dataElementName; + } + return extensionName; + } + + private ElementDefinition getDifferentialElement(StructureDefinition sd, String elementId) { + ElementDefinition element = null; + for (ElementDefinition ed : sd.getDifferential().getElement()) { + if (ed.getId().equals(elementId)) { + element = ed; + break; + } + } + return element; + } + + private void applyDataElementToElementDefinition(DictionaryElement element, StructureDefinition sd, ElementDefinition ed) { + ed.setShort(element.getDataElementLabel()); + ed.setLabel(element.getDataElementName()); + ed.setComment(element.getNotes()); + ElementDefinition.ElementDefinitionMappingComponent mapping = new ElementDefinition.ElementDefinitionMappingComponent(); + mapping.setIdentity(element.getScope()); + mapping.setMap(element.getId()); + ed.addMapping(mapping); + + // Add the element to set of elements for this profile + List lde = elementsByProfileId.get(sd.getId()); + if (lde == null) { + lde = new ArrayList<>(); + elementsByProfileId.put(sd.getId(), lde); + lde.add(element); + } + else { + if (!lde.contains(element)) { + lde.add(element); + } + } + + // Record the profile in which the data element is present: + profilesByElementId.put(element.getId(), sd); + } + + private CanonicalResourceAtlas getAtlas() { + if (atlas == null) { + atlas = new CanonicalResourceAtlas() + .setValueSets(new InMemoryCanonicalResourceProvider<>(this.valueSets)) + .setCodeSystems(new InMemoryCanonicalResourceProvider<>(this.codeSystems)) + .setConceptMaps(new InMemoryCanonicalResourceProvider<>(this.conceptMaps.values())); + } + return atlas; + } + + private String getExtensionsPath(String scopePath) { + return scopePath + "/input/extensions"; + } + + private void ensureExtensionsPath(String scopePath) { + String extensionsPath = getExtensionsPath(scopePath); + ensurePath(extensionsPath); + } + + /* Write Methods */ + public void writeResource(String path, Resource resource) { + String outputFilePath = path + "/" + resource.getResourceType().toString().toLowerCase() + "-" + resource.getIdElement().getIdPart() + "." + encoding; + try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { + writer.write( + encoding.equals("json") + ? FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + : FhirContext.forR4Cached().newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + ); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing resource: " + resource.getIdElement().getIdPart()); + } + } + + private String getProfilesPath(String scopePath) { + return scopePath + "/input/profiles"; + } + + private void ensureProfilesPath(String scopePath) { + String profilesPath = getProfilesPath(scopePath); + ensurePath(profilesPath); + } + + private void indexProfile(StructureDefinition sd) { + // Index the profile by Activity Id + Coding activityCoding = getActivityCoding(sd); + if (activityCoding != null) { + indexProfileByActivity(activityCoding, sd); + } + // Index the profile by Parent profile + String parentUrl = sd.getBaseDefinition(); + if (parentUrl != null) { + indexProfileByParent(parentUrl, sd); + } + } + + private String getCodeSystemPath(String scopePath) { + return scopePath + "/input/vocabulary/codesystem"; + } + + private void ensureCodeSystemPath(String scopePath) { + String codeSystemPath = getCodeSystemPath(scopePath); + ensurePath(codeSystemPath); + } + + private String getValueSetPath(String scopePath) { + return scopePath + "/input/vocabulary/valueset"; + } + + private void ensureValueSetPath(String scopePath) { + String valueSetPath = getValueSetPath(scopePath); + ensurePath(valueSetPath); + } + + private String getConceptMapPath(String scopePath) { + return scopePath + "/input/vocabulary/conceptmap"; + } + + private void ensureConceptMapPath(String scopePath) { + String conceptMapPath = getConceptMapPath(scopePath); + ensurePath(conceptMapPath); + } + + private String getQuestionnairePath(String scopePath) { + return scopePath + "/input/resources/questionnaire"; + } + + private void ensureQuestionnairePath(String scopePath) { + String questionnairePath = getQuestionnairePath(scopePath); + ensurePath(questionnairePath); + } + + private String getExamplesPath(String scopePath) { + return scopePath + "/input/examples"; + } + + private void ensureExamplesPath(String scopePath) { + String examplesPath = getExamplesPath(scopePath); + ensurePath(examplesPath); + } + + private String getTestsPath(String scopePath) { + return scopePath + "/input/tests"; + } + + private void ensureTestsPath(String scopePath) { + String testsPath = getTestsPath(scopePath); + ensurePath(testsPath); + } + + private String getTestPath(String scopePath, String testId) { + return scopePath + "/input/tests/" + testId; + } + + private void ensureTestPath(String scopePath, String testId) { + String testPath = getTestPath(scopePath, testId); + ensurePath(testPath); + } + + public String getCodeSystemIdentifier(CodeSystem cs) { + if (cs != null) { + String identifier = cs.hasTitle() ? cs.getTitle() : cs.getName(); + if (cs.hasVersion()) { + identifier = String.format("%s (%s)", identifier, cs.getVersion()); + } + + return identifier; + } + + return null; + } + + public String getCodeSystemIdentifier(Coding coding) { + CodeSystem result = null; + for (CodeSystem cs : codeSystems) { + if (coding.getSystem().equals(cs.getUrl())) { + if (coding.hasVersion() && cs.hasVersion() && coding.getVersion().equals(cs.getVersion())) { + result = cs; + break; + } + + if (!coding.hasVersion() && !cs.hasVersion()) { + result = cs; + break; + } + + // TODO: Use a terminology service to resolve this? + } + } + + if (result != null) { + return getCodeSystemIdentifier(result); + } + + return getCodeSystemIdentifier(coding.getSystem()); + } + + public String getCodeSystemIdentifier(String url) { + for (Map.Entry e : supportedCodeSystems.entrySet()) { + if (e.getValue().equals(url)) { + return e.getKey(); + } + } + + return null; + } + + private void ensureCqlPath(String scopePath) { + String cqlPath = getCqlPath(scopePath); + ensurePath(cqlPath); + } + + private String getCqlPath(String scopePath) { + return scopePath + "/input/cql"; + } + + private Coding getActivityCoding(StructureDefinition sd) { + if (sd.hasUseContext()) { + for (UsageContext uc : sd.getUseContext()) { + if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(uc.getCode().getSystem()) + && "task".equals(uc.getCode().getCode())) { + return getActivityCoding(uc.getValueCodeableConcept()); + } + } + } + + return null; + } + + private Coding getActivityCoding(CodeableConcept concept) { + if (concept.hasCoding()) { + for (Coding c : concept.getCoding()) { + if (activityCodeSystem.equals(c.getSystem())) { + return c; + } + } + } + + return null; + } + + private Coding getActivityCoding(String activityId) { + if (activityId == null || activityId.isEmpty()) { + return null; + } + + int i = activityId.indexOf(" "); + if (i <= 1) { + return null; + } + + String activityCode = activityId.substring(0, i); + String activityDisplay = activityId.substring(i + 1); + + if (activityDisplay.isEmpty()) { + return null; + } + + if (activityCode.endsWith(".")) { + activityCode = activityCode.substring(0, activityCode.length() - 1); + } + + Coding activity = activityMap.get(activityCode); + + if (activity == null) { + activity = new Coding().setCode(activityCode).setSystem(activityCodeSystem).setDisplay(activityDisplay); + activityMap.put(activityCode, activity); + } + + return activity; + } + + private String getActivityID(Row row, HashMap colIds) { + return SpreadsheetHelper.getCellAsString(row, getColId(colIds, "ActivityID")); + } + + private String getMultipleChoiceType(Row row, HashMap colIds) { + return SpreadsheetHelper.getCellAsString(row, getColId(colIds, "MultipleChoiceType")); + } + + private String getName(Row row, HashMap colIds) { + String name = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "Name")); + if (name != null) { + name = name + .replace("?", "") + .replace("–", "-"); + } + return name; + } + + private String getDataElementID(Row row, HashMap colIds) { + return SpreadsheetHelper.getCellAsString(row, getColId(colIds, "DataElementID")); + } + + private String getDataElementLabel(Row row, HashMap colIds) { + String dataElementLabel = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "DataElementLabel")); + if (dataElementLabel != null) { + dataElementLabel = dataElementLabel + .replace("?", "") + .replace("–", "-"); + } + return dataElementLabel; + } + + private List getPrimaryCodes(String elementId, String elementLabel, Row row, HashMap colIds) { + List codes; + codes = getDataElementCodes(row, colIds, elementId, elementLabel); + return codes; + } + + private DictionaryFhirElementPath getFhirElementPath(Row row, HashMap colIds) { + DictionaryFhirElementPath fhirType = null; + String resource = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4Resource")); + + if (resource != null && !resource.isEmpty()) { + resource = resource.trim(); + fhirType = new DictionaryFhirElementPath(); + fhirType.setResource(resource); + fhirType.setFhirElementType(SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4ResourceType"))); + } + return fhirType; + } + + private Questionnaire.QuestionnaireItemType getQuestionnaireItemType(DictionaryElement dataElement) { + Questionnaire.QuestionnaireItemType type = null; + + String typeString = null; + if (dataElement.getFhirElementPath() != null) { + typeString = dataElement.getFhirElementPath().getFhirElementType(); + } + + if (typeString == null || typeString.isEmpty()) { + typeString = dataElement.getType(); + } + + if (typeString == null || typeString.isEmpty()) { + logger.warn("Could not determine type for Data Element: {}.", dataElement.getDataElementLabel()); + return type; + } + + if (typeString.toLowerCase().trim().startsWith("reference(")) { + type = Questionnaire.QuestionnaireItemType.REFERENCE; + return type; + } + + switch (typeString.toLowerCase().trim()) { + case "annotation": + case "id": + case "note": + case "string": + case "text": + type = Questionnaire.QuestionnaireItemType.STRING; + break; + case "boolean": + type = Questionnaire.QuestionnaireItemType.BOOLEAN; + break; + case "date": + type = Questionnaire.QuestionnaireItemType.DATE; + break; + case "datetime": + type = Questionnaire.QuestionnaireItemType.DATETIME; + break; + case "code": + case "coded": + case "codes": + case "coding": + case "codeableconcept": + case "coding - n/a": + case "coding (select all that apply": + case "coding - select all that apply": + case "coding - select one": + type = Questionnaire.QuestionnaireItemType.CHOICE; + break; + case "int": + case "integer": + type = Questionnaire.QuestionnaireItemType.INTEGER; + break; + case "quantity": + type = Questionnaire.QuestionnaireItemType.QUANTITY; + break; + default: + logger.warn("Questionnaire Item Type not mapped: {}.", typeString); + } + + return type; + } + + private List getDataElementCodes(Row row, HashMap colIds, String elementId, String elementLabel) { + List codes = new ArrayList<>(); + + if (enableOpenMRS) { + // Open MRS choices + 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(elementId, elementLabel, row, colIds); + codes.addAll(fhirCodes); + //} + + // Other Terminology choices + for (String codeSystemKey : supportedCodeSystems.keySet()) { + List codeSystemCodes = getTerminologyCodes(codeSystemKey, elementId, elementLabel, row, colIds); + if (codes != codeSystemCodes && !codeSystemCodes.isEmpty()) { + for (DictionaryCode c : codes) { + c.getMappings().addAll(codeSystemCodes); + } + } + } + + return codes; + } + + @Nonnull + private StructureDefinition createProfileStructureDefinition(DictionaryElement element, String customProfileId) { + DictionaryFhirElementPath elementPath = element.getFhirElementPath(); + String customProfileIdRaw = element.getCustomProfileId(); + boolean hasCustomProfileIdRaw = customProfileIdRaw != null && !customProfileIdRaw.isEmpty(); + String resourceType = elementPath.getResourceType().trim(); + Coding activityCoding = getActivityCoding(element.getActivity()); + + StructureDefinition sd; + sd = new StructureDefinition(); + sd.setId(customProfileId); + sd.setUrl(String.format("%s/StructureDefinition/%s", canonicalBase, customProfileId)); + // TODO: version (I think this needs to come from the IG version, we don't need to set that here) + sd.setName(toName(hasCustomProfileIdRaw ? customProfileIdRaw : element.getName())); + sd.setTitle(hasCustomProfileIdRaw ? customProfileIdRaw : element.getLabel()); + + //if (element.getId() != null) { + // sd.addIdentifier(new Identifier().setUse(Identifier.IdentifierUse.OFFICIAL) + // .setSystem(dataElementIdentifierSystem) + // .setValue(element.getId()) + // ); + //} + + StructureDefinition.StructureDefinitionMappingComponent mapping = new StructureDefinition.StructureDefinitionMappingComponent(); + mapping.setIdentity(element.getScope()); + // TODO: Data Element mapping... + mapping.setUri("https://www.who.int/publications/i/item/9789240020306"); + mapping.setName("Digital Adaptation Kit for Antenatal Care"); + sd.addMapping(mapping); + + sd.setStatus(Enumerations.PublicationStatus.DRAFT); + sd.setExperimental(false); + // TODO: date // Should be set by publication tooling + // TODO: publisher // Should be set by publication tooling + // TODO: contact // Should be set by publication tooling + if (hasCustomProfileIdRaw) { + sd.setDescription(customProfileIdRaw); + } + else { + sd.setDescription(element.getDescription()); + } + + // TODO: What to do with Notes? // We should add any warnings generated during this process to the notes... + sd.setFhirVersion(Enumerations.FHIRVersion._4_0_1); + sd.setKind(StructureDefinition.StructureDefinitionKind.RESOURCE); + sd.setAbstract(false); + + if (activityCoding != null) { + sd.addUseContext(new UsageContext() + .setCode(new Coding() + .setCode("task") + .setSystem("http://terminology.hl7.org/CodeSystem/usage-context-type") + .setDisplay("Workflow Task") + ).setValue(new CodeableConcept().addCoding(activityCoding))); + } + + sd.setType(resourceType); + + String baseResource = "http://hl7.org/fhir/StructureDefinition/" + resourceType; + String baseProfileValue = element.getBaseProfile(); + if (baseProfileValue == null || baseProfileValue.isEmpty() || requiresExtension(element) || + baseProfileValue.equalsIgnoreCase("fhir")) { + sd.setBaseDefinition(baseResource); + } + else { + sd.setBaseDefinition(baseProfileValue); + } + + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setDifferential(new StructureDefinition.StructureDefinitionDifferentialComponent()); + + // Add root element + ElementDefinition ed = new ElementDefinition(); + ed.setId(resourceType); + ed.setPath(resourceType); + ed.setMustSupport(false); + sd.getDifferential().addElement(ed); + // If this data element is only a root data element, apply the element information here + //if (element.getFhirElementPath() != null && element.getFhirElementPath().getResourceTypeAndPath().equals(resourceType)) { + // ed.setShort(element.getDataElementLabel()); + // ed.setLabel(element.getDataElementName()); + // ed.setComment(element.getNotes()); + //} + + // TODO: status + // TODO: category + // TODO: subject + // TODO: effective[x] + + return sd; + } + + private boolean requiresExtension(DictionaryElement element) { + String extensionNeededValue = element.getExtensionNeeded(); + return toBoolean(extensionNeededValue); + } + + private StructureDefinition ensureExtension(DictionaryElement element) { + StructureDefinition sd = null; + + String extensionName = getExtensionName(element.getFhirElementPath().getResourcePath(), + element.getDataElementName()); + + // Search for extension and use it if it exists already. + String extensionId = IDUtils.toId(extensionName, isNumericIdAllowed()); + if (extensionId.length() > 0) { + for (StructureDefinition existingExtension : extensions) { + if (existingExtension.getId().equals(existingExtension)) { + sd = existingExtension; + } + } + } else { + throw new IllegalArgumentException("No name specified for the element"); + } + + // If the extension doesn't exist, create it with the root element. + if (sd == null) { + sd = createExtensionStructureDefinition(element, extensionId); + } + + ensureChoicesDataElement(element, sd); + + if (!extensions.contains(sd)) { + extensions.add(sd); + } + + return sd; + } + + private void ensureElement(DictionaryElement element, StructureDefinition sd) { + String codePath = null; + String choicesPath; + + DictionaryFhirElementPath elementPath = element.getFhirElementPath(); + String resourceType = elementPath.getResourceType().trim(); + + switch (resourceType) { + case "AllergyIntolerance": + case "Observation": + codePath = "code"; + choicesPath = elementPath.getResourcePath(); + break; + case "Appointment": + case "CarePlan": + case "Communication": + case "Condition": + case "Consent": + case "Coverage": + case "DeviceUseStatement": + case "DocumentReference": + case "Encounter": + case "HealthcareService": + case "Immunization": + case "Location": + case "Medication": + case "MedicationAdministration": + case "MedicationDispense": + case "MedicationRequest": + case "MedicationStatement": + case "OccupationalData": + case "Organization": + case "Patient": + case "Practitioner": + case "PractitionerRole": + case "Procedure": + case "ServiceRequest": + case "Specimen": + choicesPath = elementPath.getResourcePath(); + break; + default: + throw new IllegalArgumentException("Unrecognized baseType: " + resourceType); + } + + try { + // if (codePath != null && choicesPath.equals(codePath)) { + // Consolidate getPrimaryCodes() and choicesCodes somehow and bind that VS to the choicesPath + + // For Observations, it is a valid scenario for the Data Dictionary (DD) to not have a Data Element entry for the primary code path element - Observation.code. + // In this case, the tooling should ensure that this element is created. The code element should be bound to a ValueSet that contains all codes + // specified by the Data Element record mapped to Observation.value[x]. For all other resource types it is invalid to not have a primary + // code path element entry in the DD + boolean primaryCodePathElementAdded = false; + if (codePath != null && element.getPrimaryCodes() != null) { +// && element.getPrimaryCodes().getCodes().size() > 0 +// && !choicesPath.equalsIgnoreCase(codePath)) { + String elementId = String.format("%s.%s", resourceType, codePath); + String primaryCodePath = String.format("%s.%s", resourceType, codePath); + + ElementDefinition existingPrimaryCodePathElement = getDifferentialElement(sd, elementId); + + boolean isPrimaryCodePath = element.getFhirElementPath().getResourceTypeAndPath().equals(primaryCodePath); + boolean isPreferredCodePath = isPrimaryCodePath || element.getFhirElementPath().getResourceTypeAndPath().equals("Observation.value[x]"); + + if (existingPrimaryCodePathElement == null) { + ElementDefinition ed = new ElementDefinition(); + ed.setId(elementId); + ed.setPath(elementId); + ed.setMin(1); + ed.setMax("1"); + ed.setMustSupport(true); + if (isPreferredCodePath) { + ensureTerminologyAndBindToElement(element, sd, ed, null, null, true); + primaryCodePathElementAdded = true; + } + + addElementToStructureDefinition(sd, ed); + applyDataElementToElementDefinition(element, sd, ed); + } else { + Type existingCode = existingPrimaryCodePathElement.getFixed(); + // The code in the Primary Code Path Data Element entry should always have priority over the preferred (value[x]) + if ((existingCode == null || isPrimaryCodePath) && (isPreferredCodePath)) { + //TODO: Bind to valueset rather than fixed code + existingPrimaryCodePathElement.setFixed(element.getPrimaryCodes().getCodes().get(0).toCodeableConcept()); + } + } + } + + Boolean isSlice = element.getMasterDataType().equalsIgnoreCase("slice"); + String masterDataElementPath = element.getMasterDataElementPath(); + Boolean isElementOfSlice = !isSlice && masterDataElementPath != null && masterDataElementPath.contains("."); + + String elementId; + String slicePath; + String sliceName = null; + if (isSlice || isElementOfSlice) { + int periodIndex = masterDataElementPath.indexOf("."); + sliceName = periodIndex > 0 ? masterDataElementPath.substring(0, periodIndex) : masterDataElementPath; + slicePath = periodIndex > 0 ? masterDataElementPath.substring(periodIndex + 1) : masterDataElementPath; + + String resource = elementPath.getResourceTypeAndPath(); + int elementPathStartIndex = resource.indexOf(slicePath); + if (Boolean.TRUE.equals(isSlice)) { + elementId = String.format("%s:%s", resource, sliceName); + } else { + elementId = String.format("%s:%s.%s", resource.substring(0, elementPathStartIndex - 1), sliceName, resource.substring(elementPathStartIndex)); + } + } else { +// if (isChoiceType(elementPath)) { +// String elementFhirType = getFhirTypeOfTargetElement(elementPath); +// elementFhirType = elementFhirType.substring(0, 1).toUpperCase() + elementFhirType.substring(1); +// elementId = elementPath.getResourceTypeAndPath().replace("[x]", elementFhirType); +// } else { + elementId = String.format("%s.%s", resourceType, choicesPath); +// } + } + + ElementDefinition existingElement = getDifferentialElement(sd, elementId); + + // if the element doesn't exist, create it + if (existingElement == null) { + if (Boolean.TRUE.equals(isSlice)) { + ensureSliceAndBaseElementWithSlicing(element, elementPath, sd, elementId, sliceName, null); + } else { + String elementFhirType = getFhirTypeOfTargetElement(elementPath); + + // Split the elementPath on . then ensure an element for all between 1st and last. + String[] pathParts = elementPath.getResourcePath().split("\\."); + if (pathParts.length > 1) { + List pathPartsCumulative = new ArrayList<>(); + pathPartsCumulative.add(elementPath.getResourceType()); + for (int i = 0; i < pathParts.length - 1; i++) { + pathPartsCumulative.add(pathParts[i]); + String path = String.join(".", String.join(".", pathPartsCumulative)); + String id = path; + ElementDefinition pathElement = new ElementDefinition(); + pathElement.setId(id); + pathElement.setPath(path); + + ElementDefinition existing = getDifferentialElement(sd, id); + if (existing == null) { + addElementToStructureDefinition(sd, pathElement); + } + } + } + + ElementDefinition ed = new ElementDefinition(); + ed.setId(elementId); + ed.setPath(elementId); + ed.setMin((toBoolean(element.getRequired()) || isRequiredElement(element)) ? 1 : 0); + // BTR-> This will almost always be 1, and I don't think we currently have any where it wouldn't, because a + // multiple choice element would actually be multiple observations, rather than a single observation with multiple values + ed.setMax("1"); //isMultipleChoiceElement(element) ? "*" : "1"); + ed.setMustSupport(true); + + ElementDefinition.TypeRefComponent edtr = new ElementDefinition.TypeRefComponent(); + if (elementFhirType != null && elementFhirType.length() > 0) { + edtr.setCode(elementFhirType); + ed.addType(edtr); + } + + // If this is an Observation and we've already created the primary code path element, do not bind the + // targeted/mapped element (e.g., value[x]) to the primary codes valueset - that was done above + if (!primaryCodePathElementAdded) {// && codePath != null) { + ensureTerminologyAndBindToElement(element, sd, ed, null, null, true); + } + addElementToStructureDefinition(sd, ed); + applyDataElementToElementDefinition(element, sd, ed); + + // UnitOfMeasure-specific block + String unitOfMeasure = element.getUnitOfMeasure(); + boolean hasUnitOfMeasure = unitOfMeasure != null && !unitOfMeasure.isEmpty(); + if (isChoiceType(elementPath) && hasUnitOfMeasure) { + ElementDefinition unitElement = new ElementDefinition(); + unitElement.setId(elementId + ".unit"); + unitElement.setPath(elementId + ".unit"); + unitElement.setMin(1); + unitElement.setMax("1"); + unitElement.setMustSupport(true); + + //TODO: This should be a code, not fixed string + ElementDefinition.TypeRefComponent uitr = new ElementDefinition.TypeRefComponent(); + if (elementFhirType != null && elementFhirType.length() > 0) { + uitr.setCode("string"); + unitElement.addType(uitr); + } + unitElement.setFixed(new StringType(unitOfMeasure)); + + addElementToStructureDefinition(sd, unitElement); + } + } + } else { + // If this is a choice type, append the current element's type to the type list. + if (isChoiceType(elementPath)) { + List existingTypes = existingElement.getType(); + + ElementDefinition.TypeRefComponent elementType = null; + String elementFhirType = getFhirTypeOfTargetElement(elementPath); + if (elementFhirType != null && elementFhirType.length() > 0) { + for (ElementDefinition.TypeRefComponent type : existingTypes) { + if (type.getCode().equals(elementFhirType)) { + elementType = type; + break; + } + } + } + + if (elementType == null) { + elementType = new ElementDefinition.TypeRefComponent(); + elementType.setCode(elementFhirType); + existingElement.addType(elementType); + } + + // If this is an Observation and we've already created the primary code path element, do not bind the + // targeted/mapped element (e.g., value[x]) to the primary codes valueset - that was done above + if (!primaryCodePathElementAdded) {// && codePath != null) { + ensureTerminologyAndBindToElement(element, sd, existingElement, null, null, true); + } + } + } + + ensureChoicesDataElement(element, sd); + + } catch (Exception e) { + logger.error("Error ensuring element for '{}'. Error: {} ", element.getLabel(), e); + } + } + + private void indexProfileByActivity(Coding activityCoding, StructureDefinition sd) { + String activityId = activityCoding.getCode(); + if (activityId != null) { + List sds = profilesByActivityId.computeIfAbsent(activityId, k -> new ArrayList<>()); + if (!sds.contains(sd)) { + sds.add(sd); + } + } + } + + private void indexProfileByParent(String parentUrl, StructureDefinition sd) { + List sds = profilesByParentProfile.computeIfAbsent(parentUrl, k -> new ArrayList<>()); + if (!sds.contains(sd)) { + sds.add(sd); + } + } + + 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")); + String display = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "OpenMRSEntity")); + String codeListString = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "OpenMRSEntityId")); + if (codeListString != null && !codeListString.isEmpty()) { + String[] codesList = codeListString.split(";"); + + for (String c : codesList) { + codes.add(getCode(system, elementId, elementLabel, display, c, parent, "equivalent")); + } + } + return cleanseCodes(codes); + } + + 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 + if (system == null || system.isEmpty()) { + system = SpreadsheetHelper.getCellAsString(currentInputOptionParentRow, getColId(colIds, "FhirCodeSystem")); + } + String display = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4CodeDisplay")); + // If there is no display, use the data element label + if (display == null || display.isEmpty()) { + display = label; + } + String parentLabel = null; + String parentName = getDataElementLabel(currentInputOptionParentRow, colIds); + if (parentName != null) { + parentName = parentName.trim(); + if (!parentName.trim().isEmpty()) { + parentName = parentName.trim(); + DictionaryElement currentElement = elementMap.get(parentName); + if (currentElement != null) { + parentLabel = currentElement.getDataElementLabel(); + } + } + } + if (system != null && !system.isEmpty()) { + String codeListString = SpreadsheetHelper.getCellAsString(row, getColId(colIds, "FhirR4Code")); + // If there is no code, use the data element label, prefixed with the parentLabel, if there is one + if (codeListString == null || codeListString.isEmpty()) { + codeListString = id; + //codeListString = parentId != null ? (parentId + '-' + id) : id; + } + if (codeListString != null && !codeListString.isEmpty()) { + String[] codesList = codeListString.split(";"); + for (String c : codesList) { + codes.add(getCode(system, id, label, display, c, null, null)); + } + } + + if (system.startsWith(projectCodeSystemBase)) { + CodeSystem codeSystem = null; + for (CodeSystem cs : codeSystems) { + if (cs.getUrl().equals(system)) { + codeSystem = cs; + } + } + + if (codeSystem == null) { + String codeSystemName = system.substring(system.indexOf("CodeSystem/") + "CodeSystem/".length()); + codeSystem = createCodeSystem(codeSystemName, projectCodeSystemBase, "Extended Codes CodeSystem", + "Set of codes representing all concepts used in the implementation guide"); + } + + for (DictionaryCode code : codes) { + CodeSystem.ConceptDefinitionComponent concept = new CodeSystem.ConceptDefinitionComponent(); + concept.setCode(code.getCode()); + concept.setDisplay(code.getLabel()); + + String definition = parentLabel != null ? String.format("%s - %s", parentLabel, code.getLabel()) + : code.getLabel(); + concept.setDefinition(definition); + codeSystem.addConcept(concept); + } + } + } + return cleanseCodes(codes); + } + + 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)); + if (codeListString != null && !codeListString.isEmpty()) { + String[] codesList = codeListString.split(";"); + String display; + for (String c : codesList) { + display = getCodeComments(row, colIds, getCodeSystemCommentColName(codeSystemKey)); + int bestFitIndex = display != null ? display.toLowerCase().indexOf("?best fit") : -1; + if (bestFitIndex < 0) { + bestFitIndex = display != null ? display.toLowerCase().indexOf("??note: best fit") : -1; + } + String equivalence = "equivalent"; + if (bestFitIndex > 0) { + display = display.substring(0, bestFitIndex); + equivalence = "relatedto"; + } + codes.add(getCode(system, id, label, display, c, null, equivalence)); + } + } + + return cleanseCodes(codes); + } + + private String toName(String name) { + String result = IDUtils.toUpperId(name, isNumericIdAllowed()); + if (result.isEmpty()) { + return result; + } + if (Character.isDigit(result.charAt(0))) { + return "_" + result; + } + return result; + } + + private boolean toBoolean(String value) { + return value != null && !value.isEmpty() + && ("Yes".equalsIgnoreCase(value) || "R".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)); + } + + private StructureDefinition createExtensionStructureDefinition(DictionaryElement element, String extensionId) { + // DictionaryFhirElementPath elementPath = element.getFhirElementPath(); + + StructureDefinition sd; + sd = new StructureDefinition(); + sd.setId(extensionId); + sd.setUrl(String.format("%s/StructureDefinition/%s", canonicalBase, sd.getId())); + // TODO: version + + String extensionName = getExtensionName(element.getFhirElementPath().getResourcePath(), + element.getDataElementName()); + sd.setName(extensionName); + sd.setTitle(element.getLabel()); + sd.setStatus(Enumerations.PublicationStatus.DRAFT); + sd.setExperimental(false); + // TODO: date + // TODO: publisher + // TODO: contact + sd.setDescription(element.getDescription()); + // TODO: What to do with Notes? + sd.setFhirVersion(Enumerations.FHIRVersion._4_0_1); + sd.setKind(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE); + sd.setAbstract(false); + + StructureDefinition.StructureDefinitionContextComponent context = new StructureDefinition.StructureDefinitionContextComponent(); + context.setType(StructureDefinition.ExtensionContextType.ELEMENT); + context.setExpression(element.getFhirElementPath().getResourceType()); + List contextList = new ArrayList<>(); + contextList.add(context); + sd.setContext(contextList); + + sd.setType("Extension"); + sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension"); + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + sd.setDifferential(new StructureDefinition.StructureDefinitionDifferentialComponent()); + + // TODO: status + // TODO: category + // TODO: subject + // TODO: effective[x] + + // Add root element + ElementDefinition rootElement = new ElementDefinition(); + rootElement.setId("Extension"); + rootElement.setPath("Extension"); + rootElement.setShort(element.getDataElementLabel()); + rootElement.setLabel(element.getDataElementName()); + rootElement.setComment(element.getNotes()); + rootElement.setDefinition(element.getDescription()); + rootElement.setMin(toBoolean(element.getRequired()) ? 1 : 0); + rootElement.setMax(isMultipleChoiceElement(element) ? "*" : "1"); + + sd.getDifferential().addElement(rootElement); + + // Add extension element + ElementDefinition extensionElement = new ElementDefinition(); + extensionElement.setId("Extension.extension"); + extensionElement.setPath("Extension.extension"); + extensionElement.setMin(toBoolean(element.getRequired()) ? 1 : 0); + extensionElement.setMax(isMultipleChoiceElement(element) ? "*" : "1"); + sd.getDifferential().addElement(extensionElement); + + // Add url element + ElementDefinition urlElement = new ElementDefinition(); + urlElement.setId("Extension.url"); + urlElement.setPath("Extension.url"); + urlElement.setFixed(new UriType(sd.getUrl())); + sd.getDifferential().addElement(urlElement); + + // Add value[x] element + ElementDefinition valueElement = new ElementDefinition(); + valueElement.setId("Extension.value[x]"); + valueElement.setPath("Extension.value[x]"); + + ElementDefinition.TypeRefComponent valueTypeRefComponent = new ElementDefinition.TypeRefComponent(); + String fhirType = cleanseFhirType(getExtensionFhirType(element.getType())); + + if (fhirType != null && !fhirType.isEmpty()) { + valueTypeRefComponent.setCode(fhirType); + List valueTypeList = new ArrayList<>(); + valueTypeList.add(valueTypeRefComponent); + valueElement.setType(valueTypeList); + } + + ensureTerminologyAndBindToElement(element, sd, valueElement, null, null,false); + + valueElement.setShort(element.getLabel()); + valueElement.setDefinition(element.getDescription()); + valueElement.setMin(1); + sd.getDifferential().addElement(valueElement); + + return sd; + } + + private void ensureChoicesDataElement(DictionaryElement dictionaryElement, StructureDefinition sd) { + if (dictionaryElement.getChoices() != null && dictionaryElement.getChoices().getFhirElementPath() != null) { + String choicesElementId = dictionaryElement.getChoices().getFhirElementPath().getResourceTypeAndPath(); + ElementDefinition existingChoicesElement = getDifferentialElement(sd, choicesElementId); + + ValueSet valueSetToBind = null; + + Map> valueSetCodes = dictionaryElement.getChoices().getValueSetCodes(); + String parentCustomValueSetName = dictionaryElement.getCustomValueSetName(); + if (parentCustomValueSetName == null || parentCustomValueSetName.isEmpty()) { + parentCustomValueSetName = dictionaryElement.getDataElementLabel(); + } + + List theValueSets = new ArrayList<>(); + for (Map.Entry> vs: valueSetCodes.entrySet()) { + ValueSet valueSet = ensureValueSetWithCodes(getValueSetId(vs.getKey()), vs.getKey(), new CodeCollection(vs.getValue())); + theValueSets.add(valueSet); + + valueSetToBind = valueSet; + } + + if (valueSetCodes.size() > 1) { + String choicesGrouperValueSetName = parentCustomValueSetName + " Choices Grouper"; + String choicesGrouperValueSetId = dictionaryElement.getId() + "-choices-grouper"; + valueSetNameMap.put(choicesGrouperValueSetName, choicesGrouperValueSetId); + valueSetToBind = createGrouperValueSet(getValueSetId(choicesGrouperValueSetName), choicesGrouperValueSetName, theValueSets); + } + + //TODO: Include the primaryCodes valueset in the grouper. Add the codes to the VS in the single VS case. +// if (dictionaryElement.getFhirElementPath().getResourceTypeAndPath().equalsIgnoreCase(choicesElementId)) { +// List primaryCodes = dictionaryElement.getPrimaryCodes().getCodes(); +// codes.getCodes().addAll(primaryCodes); +// } + + if (existingChoicesElement != null) { + ElementDefinition.ElementDefinitionBindingComponent existingBinding = existingChoicesElement.getBinding(); + if (existingBinding == null || existingBinding.getId() == null) { + bindValueSetToElement(existingChoicesElement, valueSetToBind, dictionaryElement.getBindingStrength()); + bindQuestionnaireItemAnswerValueSet(dictionaryElement, valueSetToBind); + } + } else { + ElementDefinition ed = new ElementDefinition(); + ed.setId(choicesElementId); + String choicesElementPath = choicesElementId; + + // If the Id is one of an extension element, that path should not include the slice name + if (choicesElementId.contains(":")) { + String[] pathParts = choicesElementId.split("\\."); + List outputPathParts = new ArrayList<>(); + for (String pathElement : pathParts) { + String[] components = pathElement.split(":"); + outputPathParts.add(components[0]); + } + + choicesElementPath = String.join(".", outputPathParts); + } + + ed.setPath(choicesElementPath); + ed.setMin(1); + // BTR-> This will almost always be 1, and I don't think we currently have any where it wouldn't, because a + // multiple choice element would actually be multiple observations, rather than a single observation with multiple values + ed.setMax("1"); //isMultipleChoiceElement(dictionaryElement) ? "*" : "1"); + ed.setMustSupport(true); + + String elementFhirType = getFhirTypeOfTargetElement(dictionaryElement.getFhirElementPath()); + ElementDefinition.TypeRefComponent tr = new ElementDefinition.TypeRefComponent(); + if (elementFhirType != null && elementFhirType.length() > 0) { + tr.setCode(elementFhirType); + ed.addType(tr); + } + + bindQuestionnaireItemAnswerValueSet(dictionaryElement, valueSetToBind); + + bindValueSetToElement(ed, valueSetToBind, dictionaryElement.getBindingStrength()); + addElementToStructureDefinition(sd, ed); + applyDataElementToElementDefinition(dictionaryElement, sd, ed); + } + } + } + + private void ensureTerminologyAndBindToElement(DictionaryElement dictionaryElement, StructureDefinition targetStructureDefinition, + ElementDefinition targetElement, CodeCollection codes, String customValueSetName, + Boolean isPrimaryDataElement) { + // Can only bind bindable types (e.g., CodeableConcept). + // 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 = IDUtils.toId(dictionaryElement.getId(), isNumericIdAllowed()); + String valueSetLabel = dictionaryElement.getLabel(); + String valueSetName = null; + + if (customValueSetName != null && !customValueSetName.isEmpty()) { + valueSetName = customValueSetName; + valueSetLabel = customValueSetName; + } + + if (valueSetName == null) { + valueSetName = dictionaryElement.getCustomValueSetName(); + valueSetLabel = dictionaryElement.getCustomValueSetName(); + } + + if (valueSetName == null || valueSetName.isEmpty()) { + valueSetName = dictionaryElement.getName(); + valueSetLabel = dictionaryElement.getName(); + } + + CodeCollection codesToBind = codes; + if (codesToBind == null || codesToBind.size() == 0) { + codesToBind = dictionaryElement.getPrimaryCodes(); + } + + valueSetNameMap.put(valueSetName, valueSetId); + ValueSet valueSet = null; + if (codesToBind != null) { + valueSet = ensureValueSetWithCodes(getValueSetId(valueSetName), valueSetLabel, codesToBind); + } + + if (valueSet != null) { + + Enumerations.BindingStrength bindingStrength = dictionaryElement.getBindingStrength(); + // Bind the current element to the valueSet + bindValueSetToElement(targetElement, valueSet, bindingStrength); + + if (!targetElement.getPath().equalsIgnoreCase("observation.code")) { + bindQuestionnaireItemAnswerValueSet(dictionaryElement, valueSet); + } + + if (Boolean.TRUE.equals(isPrimaryDataElement)) { + valueSetLabel = valueSetId; + for (ValueSet vs : valueSets) { + if (vs.getId().equals(valueSetId)) { + valueSetLabel = vs.getTitle(); + } + } + + dictionaryElement.setTerminologyIdentifier(valueSetLabel); + + DictionaryFhirElementPath retrieveFhirElementPath = null; + MultipleChoiceElementChoices choices = dictionaryElement.getChoices(); + // If element has choices, set the choices FhirElementPath in the retrieveInfo + if (choices.getFhirElementPath() != null && choices.getValueSetCodes().size() > 0) { + retrieveFhirElementPath = dictionaryElement.getFhirElementPath(); + } + retrieves.add(new RetrieveInfo(targetStructureDefinition, valueSetLabel, retrieveFhirElementPath)); + } + } + } + } + + private void addElementToStructureDefinition(StructureDefinition sd, ElementDefinition ed) { + if (sd == null) { + throw new IllegalArgumentException("sd is null"); + } + if (ed == null) { + throw new IllegalArgumentException("ed is null"); + } + if (sd.getDifferential() == null) { + throw new IllegalArgumentException("sd.differential is null"); + } + // Add the element at the appropriate place based on the path + // Ideally this code would be informed by the base StructureDefinition(s), but in the absence of that, + // hard-coding some orders here based on current content patterns: + switch (ed.getPath()) { + case "MedicationRequest.dosageInstruction.timing": addAfter(sd, ed, "MedicationRequest.dosageInstruction"); break; + case "MedicationRequest.dosageInstruction.timing.repeat": addAfter(sd, ed, "MedicationRequest.dosageInstruction.timing"); break; + case "MedicationRequest.dosageInstruction.timing.repeat.periodUnit": addAfter(sd, ed, "MedicationRequest.dosageInstruction.timing.repeat"); break; + case "MedicationRequest.statusReason": addAfter(sd, ed, "MedicationRequest"); break; + case "Immunization.vaccineCode": addAfter(sd, ed, "Immunization"); break; + case "Immunization.statusReason": addAfter(sd, ed, "Immunization"); break; + case "ServiceRequest.code": addAfter(sd, ed, "ServiceRequest"); break; + case "ServiceRequest.occurrence[x]": addAfter(sd, ed, "ServiceRequest.code"); break; + case "ServiceRequest.requester": addAfter(sd, ed, "ServiceRequest.authoredOn"); break; + case "ServiceRequest.locationReference": addAfter(sd, ed, "ServiceRequest.authoredOn"); break; + case "Procedure.code": addAfter(sd, ed, "Procedure"); break; + case "Procedure.statusReason": addAfter(sd, ed, "Procedure"); break; + default: sd.getDifferential().addElement(ed); break; + } + } + + private void ensureSliceAndBaseElementWithSlicing(DictionaryElement dictionaryElement, DictionaryFhirElementPath elementPath, + StructureDefinition sd, String elementId, String sliceName, ElementDefinition elementDefinition) { + + // Ensure the base definition exists + String baseElementId = elementId.replace(":" + sliceName, ""); + ElementDefinition existingBaseDefinition = getDifferentialElement(sd, baseElementId); + + ElementDefinition.DiscriminatorType discriminatorType = ElementDefinition.DiscriminatorType.VALUE; + String discriminatorPath = dictionaryElement.getAdditionalFHIRMappingDetails().split("=")[0].trim(); + String resourceTypePath = elementPath.getResourceTypeAndPath(); + discriminatorPath = discriminatorPath.replaceAll(resourceTypePath + ".", ""); + + if (existingBaseDefinition != null) { + ensureElementHasSlicingWithDiscriminator(existingBaseDefinition, discriminatorType, discriminatorPath); + } + else { + ElementDefinition ed = new ElementDefinition(); + ed.setId(baseElementId); + ed.setPath(elementPath.getResourceTypeAndPath()); + ed.setMin((toBoolean(dictionaryElement.getRequired()) || isRequiredElement(dictionaryElement)) ? 1 : 0); + //ed.setMax(isMultipleChoiceElement(dictionaryElement) ? "*" : "1"); + ed.setMax("*"); + ed.setMustSupport(true); + + ElementDefinition.TypeRefComponent tr = new ElementDefinition.TypeRefComponent(); + String elementFhirType = getFhirTypeOfTargetElement(elementPath); + if (elementFhirType != null && elementFhirType.length() > 0) { + tr.setCode(elementFhirType); + ed.addType(tr); + } + + ensureElementHasSlicingWithDiscriminator(ed, discriminatorType, discriminatorPath); + + addElementToStructureDefinition(sd, ed); + } + + /* Add the actual Slice (e.g., telecom:Telephone1) */ + String discriminatorValue = dictionaryElement.getAdditionalFHIRMappingDetails().split("=")[1].trim(); + ElementDefinition sliceElement = new ElementDefinition(); + sliceElement.setId(elementId); + sliceElement.setSliceName(sliceName); +// sliceElement.setBase() + sliceElement.setPath(elementPath.getResourceTypeAndPath()); + // NOTE: Passing everything through as a string for now. + sliceElement.setFixed(new StringType(discriminatorValue)); + sliceElement.setMin(toBoolean(dictionaryElement.getRequired()) ? 1 : 0); + sliceElement.setMax(isMultipleChoiceElement(dictionaryElement) ? "*" : "1"); + + addElementToStructureDefinition(sd, sliceElement); + applyDataElementToElementDefinition(dictionaryElement, sd, sliceElement); + } + + private String getFhirTypeOfTargetElement(DictionaryFhirElementPath elementPath) { + try { + // String resourceType = elementPath.getResourceType().trim(); + // StructureDefinition sd = fhirModelStructureDefinitions.get(resourceType); + // + // if (sd == null) { + // System.out.println("StructureDefinition not found - " + resourceType); + // return null; + // } + String type = null; + if (isChoiceType(elementPath)) { + type = cleanseFhirType(elementPath.getFhirElementType()); + } + return type; + + // List snapshotElements = sd.getSnapshot().getElement(); + // ElementDefinition typeElement = null; + // for (ElementDefinition elementDef : snapshotElements) { + // if + // (elementDef.toString().toLowerCase().equals(elementPath.getResourceTypeAndPath().toLowerCase())) + // { + // typeElement = elementDef; + // } + // } + + // if (typeElement != null) { + // String elementType = typeElement.getType().get(0).getCode(); + // return elementType; + // } else { + // System.out.println("Could not find element: " + + // elementPath.getResourceTypeAndPath()); + // return null; + // } + } catch (Exception e) { + throw new NoSuchElementException( + "Unable to determine FHIR Type for: " + elementPath.getResourceTypeAndPath()); + } + } + + private boolean isRequiredElement(DictionaryElement element) { + if (element != null && element.getFhirElementPath() != null && element.getFhirElementPath().getResourceTypeAndPath() != null) { + switch (element.getFhirElementPath().getResourceTypeAndPath()) { + case "MedicationRequest.medication": return true; + case "MedicationRequest.medication[x]": return true; + case "Condition.code": return true; + case "Procedure.code": return true; + case "Immunization.statusReason": return true; + case "ServiceRequest.code": return true; + case "Immunization.vaccineCode": return true; + case "Patient.contact.name": return true; + case "Procedure.performed": return true; + case "Procedure.performed[x]": return true; + case "MedicationRequest.dosageInstruction.doseAndRate": return true; + case "Immunization.occurrence": return true; + case "Immunization.occurrence[x]": return true; + default: return false; + } + } + return false; + } + + private boolean isChoiceType(DictionaryFhirElementPath elementPath) { + return elementPath.getResourcePath().contains("[x]"); + } + + private DictionaryCode getCode(String system, String id, String label, String display, String codeValue, String parent, String equivalence) { + DictionaryCode code = new DictionaryCode(); + code.setId(id); + code.setLabel(label); + code.setSystem(system); + code.setDisplay(display); + code.setCode(codeValue); + code.setParent(parent); + code.setEquivalence(equivalence); + return code; + } + + private List cleanseCodes(List codes) { + // Remove "Not classifiable in" instances + codes.removeIf(c -> c.getCode().startsWith("Not classifiable in")); + return codes; + } + + @Nonnull + private CodeSystem createCodeSystem(String name, String canonicalBase, String title, String description) { + CodeSystem codeSystem = new CodeSystem(); + + codeSystem.setId(IDUtils.toId(name, isNumericIdAllowed())); + codeSystem.setUrl(String.format("%s/CodeSystem/%s", canonicalBase, codeSystem.getId())); + // TODO: version + codeSystem.setName(toName(name)); + codeSystem.setTitle(String.format("%s", title != null ? title : name)); + codeSystem.setStatus(Enumerations.PublicationStatus.DRAFT); + codeSystem.setExperimental(false); + // TODO: date + // TODO: publisher + // TODO: contact + codeSystem.setDescription(description != null ? description : String.format("Codes representing possible values for the %s element", name)); + codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + codeSystem.setCaseSensitive(true); + + codeSystems.add(codeSystem); + + return codeSystem; + } + + private String getCodeComments(Row row, HashMap colIds, String colName) { + String comments = SpreadsheetHelper.getCellAsString(row, getColId(colIds, colName)); + comments = cleanseCodeComments(comments); + return comments; + } + + private String getCodeSystemCommentColName(String codeSystem) { + switch (codeSystem) { + case "ICD-10": return "ICD-10Comments"; + case "ICD-11": return "ICD-11Comments"; + case "ICHI": return "ICHIComments"; + case "ICF": return "ICFComments"; + case "SNOMED-CT": return "SNOMEDComments"; + case "LOINC": return "LOINCComments"; + case "RXNorm": return "RXNormComments"; + case "CPT": return "CPTComments"; + case "HCPCS": return "HCPCSComments"; + case "NDC": return "NDCComments"; + default: throw new IllegalArgumentException(String.format("Unknown code system key %s", codeSystem)); + } + } + + private boolean isMultipleChoiceElement(DictionaryElement element) { + if (element.getType() == null) { + return false; + } + + switch (element.getType().toLowerCase()) { + case "mc (select multiple)": + case "coding - select all that apply": + case "coding - (select all that apply": + return true; + default: + return false; + } + } + + private String cleanseFhirType(String type) { + if (type != null && type.length() > 0) { + switch (type) { + case "Boolean": + return "boolean"; + case "Coded": + return "code"; + case "Coded variables": + case "CodableConcept": + return "CodeableConcept"; + case "DateTime": + case "DD/MM/YYYY": + return "dateTime"; + case "Integer": + case "###": + return "integer"; + case "Free text": + case "free text": + case "Text": + return "string"; + default: + return type; + } + } else { + return type; + } + } + + // TODO: why is this different from "cleanse.." above? + private String getExtensionFhirType(String type) { + if (type != null && type.length() > 0) { + switch (type) { + case "Boolean": + return "boolean"; + case "Coded": + return "code"; + case "Integer": + return "integer"; + case "Note": + case "Text": + case "text": + return "string"; + case "Time": + return "time"; + case "Coding": + case "Coding (Select all that apply": + case "Coding - Select all that apply": + case "Coding - Select One": + case "Coding - Select one": + return "CodeableConcept"; + default: + return type; + } + } else { + return type; + } + } + + @Nonnull + private ValueSet ensureValueSetWithCodes(String valueSetId, String valueSetLabel, CodeCollection codes) { + // Ensure the ValueSet + ValueSet valueSet = null; + boolean valueSetExisted = false; + for (ValueSet vs : valueSets) { + if (vs.getId().equals(valueSetId)) { + valueSet = vs; + valueSetExisted = true; + } + } + + if (valueSet == null) { + valueSet = new ValueSet(); + valueSet.setId(valueSetId); + valueSet.setUrl(String.format("%s/ValueSet/%s", canonicalBase, valueSetId)); + valueSet.setName(toName(valueSetLabel)); + valueSet.setTitle(String.format("%s", valueSetLabel)); + valueSet.setStatus(Enumerations.PublicationStatus.DRAFT); + valueSet.setExperimental(false); + valueSet.setDescription(String.format("Codes representing possible values for the %s element", valueSetLabel)); + valueSet.setImmutable(true); + } + + // Ensure Compose element + ValueSet.ValueSetComposeComponent compose = valueSet.getCompose(); + if (compose == null) { + compose = new ValueSet.ValueSetComposeComponent(); + valueSet.setCompose(compose); + } + + // Group by Supported Terminology System + for (String codeSystemUrl : codes.getCodeSystemUrls()) { + List systemCodes = codes.getCodesForSystem(codeSystemUrl); + + if (!systemCodes.isEmpty()) { + List conceptSets = compose.getInclude(); + ValueSet.ConceptSetComponent conceptSet = null; + for (ValueSet.ConceptSetComponent cs : conceptSets) { + if (cs.getSystem().equals(codeSystemUrl)) { + conceptSet = cs; + } + } + + if (conceptSet == null) { + conceptSet = new ValueSet.ConceptSetComponent(); + compose.addInclude(conceptSet); + conceptSet.setSystem(codeSystemUrl); + } + + for (DictionaryCode code : systemCodes) { + List conceptReferences = conceptSet.getConcept(); + ValueSet.ConceptReferenceComponent conceptReference = new ValueSet.ConceptReferenceComponent(); + conceptReference.setCode(code.getCode()); + conceptReference.setDisplay(code.getDisplay()); + + // Only add the concept if it does not already exist in the ValueSet (based on both Code and Display) + if (conceptReferences.stream().noneMatch(o -> o.getCode().equals(conceptReference.getCode()) + && o.getDisplay().equals(conceptReference.getDisplay()))) { + conceptSet.addConcept(conceptReference); + } + + // Add mappings for this code to the appropriate concept map + addConceptMappings(code); + } + } + } + + // If the ValueSet did not already exist, add it to the valueSets collection + if (!valueSetExisted) { + valueSets.add(valueSet); + } + return valueSet; + } + + private String getValueSetId(String valueSetName) { + String id = valueSetNameMap.get(valueSetName); + if (id == null) { + id = valueSetName; + } + return IDUtils.toId(id, isNumericIdAllowed()); + } + + @Nonnull + private ValueSet createGrouperValueSet(String valueSetId, String valueSetLabel, List valueSetsToGroup) { + // Ensure the ValueSet + ValueSet valueSet = null; + boolean valueSetExisted = false; + for (ValueSet vs : valueSets) { + if (vs.getId().equals(valueSetId)) { + valueSet = vs; + valueSetExisted = true; + } + } + + if (valueSet == null) { + valueSet = new ValueSet(); + valueSet.setId(valueSetId); + valueSet.setUrl(String.format("%s/ValueSet/%s", canonicalBase, valueSetId)); + valueSet.setName(toName(valueSetLabel)); + valueSet.setTitle(valueSetLabel); + valueSet.setStatus(Enumerations.PublicationStatus.DRAFT); + valueSet.setExperimental(false); + valueSet.setDescription(String.format("Group Valueset with codes representing possible values for the %s element", valueSetLabel)); + valueSet.setImmutable(true); + } + + valueSet.setDate(java.util.Date.from(Instant.now())); + + // Ensure Compose element + ValueSet.ValueSetComposeComponent compose = valueSet.getCompose(); + if (compose == null) { + compose = new ValueSet.ValueSetComposeComponent(); + valueSet.setCompose(compose); + } + + // Ensure Expansion element + ValueSet.ValueSetExpansionComponent targetExpansion = valueSet.getExpansion(); + if (targetExpansion == null) { + targetExpansion = new ValueSet.ValueSetExpansionComponent(); + valueSet.setExpansion(targetExpansion); + } + targetExpansion.setTimestamp(java.util.Date.from(Instant.now())); + + // Add source valueset urls to compose of the grouper and all of the compose codes to the expansion of the grouper + List includes = valueSet.getCompose().getInclude(); +// ValueSet.ValueSetExpansionComponent targetExpansion = valueSet.getExpansion(); + List targetContains = targetExpansion.getContains(); + + for (ValueSet vs: valueSetsToGroup) { + // Add source ValueSet URLs to grouper Compose + if (includes.stream().noneMatch(i -> i.hasValueSet(vs.getUrl()))) { + ValueSet.ConceptSetComponent include = new ValueSet.ConceptSetComponent(); + include.addValueSet(vs.getUrl()); + valueSet.getCompose().addInclude(include); + } + + // NOTE: Very naive implementation that assumes a compose made up of actual include concepts. That is + // a safe assumption in context of this Processor though and the ValueSets it creates at the time of this + // implementation. + if (vs.hasCompose() && vs.getCompose().hasInclude()) { + for (ValueSet.ConceptSetComponent sourceInclude : vs.getCompose().getInclude()) { + String system = sourceInclude.getSystem(); + for (ValueSet.ConceptReferenceComponent concept : sourceInclude.getConcept()) { + if (targetContains.stream().noneMatch(c -> c.getSystem().equals(system) && c.getCode().equals(concept.getCode()))) { + ValueSet.ValueSetExpansionContainsComponent newContains = new ValueSet.ValueSetExpansionContainsComponent(); + newContains.setSystem(system); + newContains.setCode(concept.getCode()); + newContains.setDisplay(concept.getDisplay()); + targetContains.add(newContains); + } + } + } + } + } + + // If the ValueSet did not already exist, add it to the valueSets collection + if (!valueSetExisted) { + valueSets.add(valueSet); + } + return valueSet; + } + + private void bindValueSetToElement(ElementDefinition targetElement, ValueSet valueSet, Enumerations.BindingStrength bindingStrength) { + ElementDefinition.ElementDefinitionBindingComponent binding = + new ElementDefinition.ElementDefinitionBindingComponent(); + binding.setStrength(bindingStrength); + binding.setValueSet(valueSet.getUrl()); + binding.addExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", valueSet.getTitleElement()); + targetElement.setBinding(binding); + } + + private void bindQuestionnaireItemAnswerValueSet(DictionaryElement dictionaryElement, ValueSet valueSetToBind) { + questionnaires.stream().filter(q -> q.getId().equalsIgnoreCase(IDUtils.toUpperId(getActivityCoding( + dictionaryElement.getPage()).getCode(), isNumericIdAllowed()))).findFirst().flatMap(questionnaire -> questionnaire.getItem() + .stream().filter(i -> i.getText().equalsIgnoreCase(dictionaryElement.getLabel())).findFirst()) + .ifPresent(questionnaireItem -> questionnaireItem.setAnswerValueSet(valueSetToBind.getUrl())); + } + + private boolean isBindableType(DictionaryElement element) { + String type = null; + if (element.getType() != null) { + type = element.getType().toLowerCase(); + } + String mappedType = null; + if (element.getFhirElementPath() != null) { + mappedType = element.getFhirElementPath().getFhirElementType(); + } + + return (type != null && type.contains("codings")) + || (mappedType != null && (mappedType.equalsIgnoreCase("CodeableConcept") + || mappedType.equalsIgnoreCase("Code"))); + } + + private void addAfter(StructureDefinition sd, ElementDefinition ed, String afterPath) { + int targetIndex = getElementIndex(sd, afterPath); + if (targetIndex >= 0) { + sd.getDifferential().getElement().add(targetIndex + 1, ed); + } + else { + sd.getDifferential().getElement().add(ed); + } + } + + private void ensureElementHasSlicingWithDiscriminator( + ElementDefinition element, ElementDefinition.DiscriminatorType discriminatorType, String discriminatorPath) { + // If the element has a slicing component, ensure the discriminator exists on it. + if (element.hasSlicing()) { + // If discriminator does not exist on the slicing component add it else do nothing + ElementDefinition.ElementDefinitionSlicingComponent existingSlicingComponent = element.getSlicing(); + ensureSlicingHasDiscriminator(existingSlicingComponent, discriminatorType, discriminatorPath); + } else { + /* Add Slicing to base element if it's not already there */ + ElementDefinition.ElementDefinitionSlicingComponent slicingComponent = new ElementDefinition.ElementDefinitionSlicingComponent(); + ensureSlicingHasDiscriminator(slicingComponent, discriminatorType, discriminatorPath); + element.setSlicing(slicingComponent); + } + } + + private String cleanseCodeComments(String rawComment) { + String result = null; + if (rawComment != null) { + result = rawComment + .replace("Code title: ", "") + .replace("Code LongName: ", ""); + } + + return result; + } + + private void addConceptMappings(DictionaryCode code) { + CodeCollection mappings = new CodeCollection(code.getMappings()); + for (String codeSystemUrl : mappings.getCodeSystemUrls()) { + List systemCodes = mappings.getCodesForSystem(codeSystemUrl); + + ConceptMap cm = getConceptMapForSystem(codeSystemUrl); + if (cm != null) { + ConceptMap.ConceptMapGroupComponent cmg = getConceptMapGroupComponent(cm, code.getSystem()); + if (cmg == null) { + cmg = cm.addGroup().setSource(code.getSystem()).setTarget(codeSystemUrl); + } + + ConceptMap.SourceElementComponent sec = cmg.addElement().setCode(code.getCode()).setDisplay(code.getDisplay()); + for (DictionaryCode systemCode : systemCodes) { + sec.addTarget() + .setCode(systemCode.getCode()) + .setDisplay(systemCode.getDisplay()) + .setEquivalence(systemCode.getEquivalence() != null + ? Enumerations.ConceptMapEquivalence.fromCode(systemCode.getEquivalence()) + : null); + } + } + } + } + + private void ensureSlicingHasDiscriminator(ElementDefinition.ElementDefinitionSlicingComponent slicingComponent, + ElementDefinition.DiscriminatorType discriminatorType, String discriminatorPath) { + + ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent discriminator = null; + if (slicingComponent.getDiscriminator().stream().noneMatch(d -> d.getType() == discriminatorType + && d.getPath().equalsIgnoreCase(discriminatorPath.toLowerCase()))) { + discriminator = new ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent(); + discriminator.setType(discriminatorType); + discriminator.setPath(discriminatorPath); + + slicingComponent.addDiscriminator(discriminator); + } + } + + /* + Not guaranteed to return a concept map, will only return for known supported code systems + */ + private ConceptMap getConceptMapForSystem(String systemUrl) { + ConceptMap cm = conceptMaps.get(systemUrl); + if (cm == null) { + String codeSystemLabel = getCodeSystemLabel(systemUrl); + IDUtils.validateId(codeSystemLabel, isNumericIdAllowed()); + if (codeSystemLabel != null) { + cm = new ConceptMap(); + cm.setId(codeSystemLabel); + cm.setUrl(String.format("%s/ConceptMap/%s", canonicalBase, codeSystemLabel)); + cm.setName(codeSystemLabel); + cm.setTitle(String.format("%s", codeSystemLabel)); + cm.setStatus(Enumerations.PublicationStatus.DRAFT); + cm.setExperimental(false); + cm.setDescription(String.format("Concept mapping from content extended codes to %s", codeSystemLabel)); + conceptMaps.put(systemUrl, cm); + } + } + + return cm; + } + + private ConceptMap.ConceptMapGroupComponent getConceptMapGroupComponent(ConceptMap cm, String sourceUri) { + for (ConceptMap.ConceptMapGroupComponent cmg : cm.getGroup()) { + if (cmg.getSource().equals(sourceUri)) { + return cmg; + } + } + + return null; + } + + private int getElementIndex(StructureDefinition sd, String path) { + for (int i = 0; i < sd.getDifferential().getElement().size(); i++) { + if (sd.getDifferential().getElement().get(i).getPath().equals(path)) { + return i; + } + } + + return -1; + } + + private String getCodeSystemLabel(String systemUrl) { + for (Map.Entry entry : supportedCodeSystems.entrySet()) { + if (entry.getValue().equals(systemUrl)) { + return entry.getKey(); + } + } + return null; + } + + private void writeActivityIndexHeader(StringBuilder activityIndex, String activityId) { + activityIndex.append(newLine); + activityIndex.append(String.format("#### %s ", activityId)); + Coding activityCoding = activityMap.get(activityId); + if (activityCoding != null) { + activityIndex.append(activityCoding.getDisplay()); + } + activityIndex.append(newLine).append(newLine); + if (activityCoding != null) { + String questionnaireId = IDUtils.toUpperId(activityCoding.getCode(), isNumericIdAllowed()); + activityIndex.append(String.format("Data elements for this activity can be collected using the [%s](Questionnaire-%s.html)", questionnaireId, questionnaireId)); + activityIndex.append(newLine).append(newLine); + } + activityIndex.append("|Id|Label|Description|Type|Profile Path|"); + activityIndex.append(newLine); + activityIndex.append("|---|---|---|---|---|"); + activityIndex.append(newLine); + } + + private void writeActivityIndexEntry(StringBuilder activityIndex, StructureDefinition sd) { + 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(newLine); + } + } + } + + private final Comparator activityIdComparator = new Comparator<>() { + private int indexOfFirstDigit(String s) { + for (int i = 0; i < s.length(); i++) { + if (Character.isDigit(s.charAt(i))) { + return i; + } + } + + return -1; + } + + @Override + public int compare(String s1, String s2) { + int s1i = indexOfFirstDigit(s1); + int s2i = indexOfFirstDigit(s2); + if (s1i <= 0 || s2i <= 0) { + return 0; + } + + String s1a = s1.substring(0, s1i); + String s2a = s2.substring(0, s2i); + int ac = s1a.compareTo(s2a); + if (ac == 0) { + String s1b = s1.substring(s1i); + String s2b = s2.substring(s2i); + String[] s1parts = s1b.split("\\."); + String[] s2parts = s2b.split("\\."); + for (int partIndex = 0; partIndex < s1parts.length; partIndex++) { + if (partIndex >= s2parts.length) { + return 1; + } + ac = Integer.compare(Integer.parseInt(s1parts[partIndex]), Integer.parseInt(s2parts[partIndex])); + if (ac != 0) { + return ac; + } + } + if (s2parts.length > s1parts.length) { + return -1; + } + + return ac; + } + else { + return ac; + } + } + }; + + public String getPathToSpreadsheet() { + return pathToSpreadsheet; + } + + public void setPathToSpreadsheet(String pathToSpreadsheet) { + this.pathToSpreadsheet = pathToSpreadsheet; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getScopes() { + return scopes; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + public String getDataElementPages() { + return dataElementPages; + } + + public void setDataElementPages(String dataElementPages) { + this.dataElementPages = dataElementPages; + } + + public String getTestCaseInput() { + return testCaseInput; + } + + public void setTestCaseInput(String testCaseInput) { + this.testCaseInput = testCaseInput; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public boolean isNumericIdAllowed() { + return Boolean.parseBoolean(numericIdAllowed); + } + + public void setNumericIdAllowed(String numericIdAllowed) { + this.numericIdAllowed = numericIdAllowed; + } + + public void setCanonicalBase(String value) { + canonicalBase = value; + projectCodeSystemBase = canonicalBase; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessDecisionTables.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessDecisionTables.java new file mode 100644 index 000000000..86b2185fa --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/ProcessDecisionTables.java @@ -0,0 +1,730 @@ +package org.opencds.cqf.tooling.operations.acceleratorkit; + +import ca.uhn.fhir.context.FhirContext; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.hl7.fhir.r4.model.Attachment; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Expression; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.PlanDefinition; +import org.hl7.fhir.r4.model.RelatedArtifact; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.TriggerDefinition; +import org.hl7.fhir.r4.model.UsageContext; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.terminology.SpreadsheetHelper; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.opencds.cqf.tooling.utilities.IOUtils.ensurePath; + +@Operation(name = "ProcessDecisionTables") +public class ProcessDecisionTables implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(ProcessDecisionTables.class); + @OperationParam(alias = { "pts", "pathtospreadsheet" }, setter = "setPathToSpreadsheet", required = true) + private String pathToSpreadsheet; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json") + private String encoding; + @OperationParam(alias = { "dtp", "decisiontablepages" }, setter = "setDecisionTablePages", + description = "comma-separated list of the names of pages in the workbook to be processed") + private String decisionTablePages; + @OperationParam(alias = { "dtpf", "decisiontablepageprefix" }, setter = "setDecisionTablePagePrefix", + description = "all pages with a name starting with this prefix will be processed") + private String decisionTablePagePrefix; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/acceleratorkit/output") + private String outputPath; + + @OperationParam(alias = {"numid", "numericidallowed"}, setter = "setNumericIdAllowed", defaultValue = "false", + description = "Determines if we want to allow numeric IDs (This overrides default HAPI behaviour") + private String numericIdAllowed; + + private static final String CANONICAL_BASE = "http://fhir.org/guides/who/anc-cds"; + private final String newLine = System.lineSeparator(); + + private final Map planDefinitions = new LinkedHashMap<>(); + private final Map libraries = new LinkedHashMap<>(); + private final Map libraryCQL = new LinkedHashMap<>(); + private final Map activityMap = new LinkedHashMap<>(); + private final Map expressionNameCounterMap = new HashMap<>(); + + @Override + public void execute() { + Workbook workbook = SpreadsheetHelper.getWorkbook(pathToSpreadsheet); + processWorkbook(workbook); + } + + private void processWorkbook(Workbook workbook) { + ensurePath(outputPath); + + // process workbook + if (decisionTablePages != null && !decisionTablePages.isEmpty()) { + for (String page : decisionTablePages.split(",")) { + processDecisionTablePage(workbook, page); + } + } + + if (decisionTablePagePrefix != null && !decisionTablePagePrefix.isEmpty()) { + Iterator sheets = workbook.sheetIterator(); + while (sheets.hasNext()) { + Sheet sheet = sheets.next(); + if (sheet.getSheetName() != null && sheet.getSheetName().startsWith(decisionTablePagePrefix)) { + processDecisionTableSheet(sheet); + } + } + } + + writePlanDefinitions(outputPath); + writePlanDefinitionIndex(outputPath); + writeLibraries(outputPath); + writeLibraryCQL(outputPath); + } + + private void processDecisionTablePage(Workbook workbook, String page) { + Sheet sheet = workbook.getSheet(page); + if (sheet == null) { + logger.warn("Sheet {} not found in the Workbook, so no processing was done.", page); + } + else { + logger.info("Processing Sheet {}.", page); + processDecisionTableSheet(sheet); + } + } + + private void processDecisionTableSheet(Sheet sheet) { + /* + Decision table general format: + Header rows: + | Decision ID | | + | Business Rule | | + | Trigger | | + | Input(s) | ... | Output | Action | Annotation | Reference | + | | ... | | | | | --> Create a row for each... + */ + + Iterator it = sheet.rowIterator(); + + while (it.hasNext()) { + Row row = it.next(); + + Iterator cells = row.cellIterator(); + while (cells.hasNext()) { + Cell cell = cells.next(); + String cellValue = cell.getStringCellValue().toLowerCase(); + if (cellValue.startsWith("decision")) { + PlanDefinition planDefinition = processDecisionTable(it, cells); + planDefinitions.put(planDefinition.getId(), planDefinition); + generateLibrary(planDefinition); + break; + } + } + } + } + + private PlanDefinition processDecisionTable(Iterator it, Iterator cells) { + PlanDefinition planDefinition = new PlanDefinition(); + + if (!cells.hasNext()) { + throw new IllegalArgumentException("Expected decision title cell"); + } + + Cell cell = cells.next(); + int headerInfoColumnIndex = cell.getColumnIndex(); + String decisionTitle = cell.getStringCellValue().trim(); + int index = decisionTitle.indexOf(' '); + if (index < 0) { + throw new IllegalArgumentException("Expected business rule title of the form ' '"); + } + String decisionIdentifier = decisionTitle.substring(0, index); + String decisionId = decisionIdentifier.replace(".", ""); + IDUtils.validateId(decisionId, isNumericIdAllowed()); + + planDefinition.setTitle(decisionTitle); + + Identifier planDefinitionIdentifier = new Identifier(); + planDefinitionIdentifier.setUse(Identifier.IdentifierUse.OFFICIAL); + planDefinitionIdentifier.setValue(decisionIdentifier); + planDefinition.getIdentifier().add(planDefinitionIdentifier); + + planDefinition.setName(decisionId); + planDefinition.setId(decisionId); + planDefinition.setUrl(CANONICAL_BASE + "/PlanDefinition/" + decisionId); + + if (!it.hasNext()) { + throw new IllegalArgumentException("Expected Business Rule row"); + } + + Row row = it.next(); + + Cell descriptionCell = row.getCell(headerInfoColumnIndex); + if (descriptionCell == null) { + throw new IllegalArgumentException("Expected Business Rule description cell"); + } + + String decisionDescription = descriptionCell.getStringCellValue(); + planDefinition.setDescription(decisionDescription); + + planDefinition.setStatus(Enumerations.PublicationStatus.ACTIVE); + planDefinition.setDate(java.util.Date.from(Instant.now())); + planDefinition.setExperimental(false); + planDefinition.setType(new CodeableConcept().addCoding(new Coding().setSystem( + "http://terminology.hl7.org/CodeSystem/plan-definition-type").setCode("eca-rule"))); + + if (!it.hasNext()) { + throw new IllegalArgumentException("Expected Trigger row"); + } + + row = it.next(); + + Cell triggerCell = row.getCell(headerInfoColumnIndex); + if (triggerCell == null) { + throw new IllegalArgumentException("Expected Trigger description cell"); + } + + String triggerName = triggerCell.getStringCellValue(); + PlanDefinition.PlanDefinitionActionComponent action = new PlanDefinition.PlanDefinitionActionComponent(); + planDefinition.getAction().add(action); + action.setTitle(decisionTitle); + + TriggerDefinition trigger = new TriggerDefinition(); + trigger.setType(TriggerDefinition.TriggerType.NAMEDEVENT); + trigger.setName(triggerName); + action.getTrigger().add(trigger); + + Coding activityCoding = getActivityCoding(triggerName); + if (activityCoding != null) { + planDefinition.addUseContext(new UsageContext() + .setCode(new Coding() + .setCode("task") + .setSystem("http://terminology.hl7.org/CodeSystem/usage-context-type") + .setDisplay("Workflow Task") + ).setValue(new CodeableConcept().addCoding(activityCoding))); + } + + if (!it.hasNext()) { + throw new IllegalArgumentException("Expected decision table header row"); + } + + row = it.next(); + + cells = row.cellIterator(); + int inputColumnIndex = -1; + int outputColumnIndex = -1; + int actionColumnIndex = -1; + int annotationColumnIndex = -1; + int referenceColumnIndex = -1; + while (cells.hasNext()) { + cell = cells.next(); + if (cell.getStringCellValue().toLowerCase().startsWith("input") + || cell.getStringCellValue().toLowerCase().startsWith("inputs") + || cell.getStringCellValue().toLowerCase().startsWith("input(s)")) { + inputColumnIndex = cell.getColumnIndex(); + } + else if (cell.getStringCellValue().toLowerCase().startsWith("output") + || cell.getStringCellValue().toLowerCase().startsWith("outputs") + || cell.getStringCellValue().toLowerCase().startsWith("output(s)")) { + outputColumnIndex = cell.getColumnIndex(); + } + else if (cell.getStringCellValue().toLowerCase().startsWith("action") + || cell.getStringCellValue().toLowerCase().startsWith("actions") + || cell.getStringCellValue().toLowerCase().startsWith("action(s)")) { + actionColumnIndex = cell.getColumnIndex(); + } + else if (cell.getStringCellValue().toLowerCase().startsWith("annotation") + || cell.getStringCellValue().toLowerCase().startsWith("annotations") + || cell.getStringCellValue().toLowerCase().startsWith("annotation(s)")) { + annotationColumnIndex = cell.getColumnIndex(); + } + else if (cell.getStringCellValue().toLowerCase().startsWith("reference") + || cell.getStringCellValue().toLowerCase().startsWith("references") + || cell.getStringCellValue().toLowerCase().startsWith("reference(s)")) { + referenceColumnIndex = cell.getColumnIndex(); + break; + } + } + + int actionId = 1; + PlanDefinition.PlanDefinitionActionComponent currentAction = null; + String currentAnnotationValue = null; + for (;;) { + PlanDefinition.PlanDefinitionActionComponent subAction = processAction(it, inputColumnIndex, outputColumnIndex, + actionColumnIndex, annotationColumnIndex, actionId, currentAnnotationValue, referenceColumnIndex); + + if (subAction == null) { + break; + } + + if (!actionsEqual(currentAction, subAction)) { + actionId++; + currentAction = subAction; + + Integer nextCounter = 1; + String actionDescription = subAction.getAction().size() > 1 + ? subAction.getAction().get(0).getTitle().replace(newLine, "") + : subAction.getDescription(); + if (!expressionNameCounterMap.containsKey(actionDescription)) { + expressionNameCounterMap.put(actionDescription, 1); + } + + nextCounter = expressionNameCounterMap.get(actionDescription); + expressionNameCounterMap.put(actionDescription, nextCounter + 1); + + actionDescription = actionDescription + (nextCounter > 1 ? String.format(" %s", nextCounter) : ""); + subAction.setDescription(actionDescription); + + currentAnnotationValue = subAction.getTextEquivalent(); + action.getAction().add(subAction); + } + else { + mergeActions(currentAction, subAction); + } + } + + return planDefinition; + } + + private void generateLibrary(PlanDefinition planDefinition) { + String id = planDefinition.getIdElement().getIdPart(); + IDUtils.validateId(id, isNumericIdAllowed()); + + Library library = new Library(); + library.getIdentifier().add(planDefinition.getIdentifierFirstRep()); + library.setId(id); + library.setName(planDefinition.getName()); + library.setUrl(CANONICAL_BASE + "/Library/" + id); + library.setTitle(planDefinition.getTitle()); + library.setDescription(planDefinition.getDescription()); + library.addContent((Attachment)new Attachment().setId("ig-loader-" + id + ".cql")); + + planDefinition.getLibrary().add((CanonicalType)new CanonicalType().setValue(library.getUrl())); + + StringBuilder cql = new StringBuilder(); + writeLibraryHeader(cql, library); + + for (PlanDefinition.PlanDefinitionActionComponent action : planDefinition.getActionFirstRep().getAction()) { + if (action.hasCondition()) { + writeActionCondition(cql, action); + } + } + + libraries.put(id, library); + libraryCQL.put(id, cql); + } + + private Coding getActivityCoding(String activityId) { + if (activityId == null || activityId.isEmpty()) { + return null; + } + + int i = activityId.indexOf(" "); + if (i <= 1) { + return null; + } + + String activityCode = activityId.substring(0, i); + String activityDisplay = activityId.substring(i + 1); + + if (activityDisplay.isEmpty()) { + return null; + } + + Coding activity = activityMap.get(activityCode); + + if (activity == null) { + String activityCodeSystem = "http://fhir.org/guides/who/anc-cds/CodeSystem/activity-codes"; + activity = new Coding().setCode(activityCode).setSystem(activityCodeSystem).setDisplay(activityDisplay); + activityMap.put(activityCode, activity); + } + + return activity; + } + + private PlanDefinition.PlanDefinitionActionComponent processAction( + Iterator<Row> it, int inputColumnIndex, int outputColumnIndex, int actionColumnIndex, + int annotationColumnIndex, int actionId, String currentAnnotationValue, int referenceColumnIndex) { + if (it.hasNext()) { + Row row = it.next(); + // If the row is not valid, do not process it. + if (!rowIsValid(row, inputColumnIndex, actionColumnIndex, annotationColumnIndex)) { + return null; + } + + Cell cell; + PlanDefinition.PlanDefinitionActionComponent action = new PlanDefinition.PlanDefinitionActionComponent(); + + action.setId(Integer.toString(actionId)); + + List<String> conditionValues = new ArrayList<>(); + for (int inputIndex = inputColumnIndex; inputIndex < outputColumnIndex; inputIndex++) { + cell = row.getCell(inputIndex); + if (cell != null) { + String inputCondition = cell.getStringCellValue(); + if (inputCondition != null && !inputCondition.isEmpty() && !inputCondition.toLowerCase().startsWith("decision")) { + conditionValues.add(inputCondition); + } + } + } + + if (conditionValues.isEmpty()) { + // No condition, no action, end of decision table + return null; + } + + StringBuilder applicabilityCondition = new StringBuilder(); + if (conditionValues.size() == 1) { + applicabilityCondition.append(conditionValues.get(0)); + } + else { + for (String conditionValue : conditionValues) { + if (applicabilityCondition.length() > 0) { + applicabilityCondition.append(String.format("%n AND ")); + } + applicabilityCondition.append("("); + applicabilityCondition.append(conditionValue); + applicabilityCondition.append(")"); + } + } + + PlanDefinition.PlanDefinitionActionConditionComponent condition = new PlanDefinition.PlanDefinitionActionConditionComponent(); + condition.setKind(PlanDefinition.ActionConditionKind.APPLICABILITY); + condition.setExpression(new Expression().setLanguage("text/cql-identifier").setDescription(applicabilityCondition.toString())); + action.getCondition().add(condition); + + List<String> actionValues = new ArrayList<>(); + for (int actionIndex = actionColumnIndex; actionIndex < annotationColumnIndex; actionIndex++) { + cell = row.getCell(actionIndex); + if (cell != null) { + String actionValue = cell.getStringCellValue(); + if (actionValue != null && !actionValue.isEmpty()) { + actionValues.add(actionValue.replace(System.getProperty(newLine), "")); + } + } + } + + String actionsDescription = String.join(" AND ", actionValues); + action.setDescription(actionsDescription); + + if (actionValues.size() == 1) { + action.setTitle(actionValues.get(0).replace(System.getProperty("line.separator"), "")); + } + else { + action.setTitle(actionValues.get(0).replace(System.getProperty("line.separator"), "")); + for (String actionValue : actionValues) { + PlanDefinition.PlanDefinitionActionComponent subAction = + new PlanDefinition.PlanDefinitionActionComponent(); + subAction.setTitle(actionValue); + action.getAction().add(subAction); + } + } + + if (annotationColumnIndex >= 0) { + cell = row.getCell(annotationColumnIndex); + if (cell != null) { + String annotationValue = cell.getStringCellValue(); + if (annotationValue != null && !annotationValue.isEmpty()) { + currentAnnotationValue = annotationValue; + } + } + } + + // TODO: Might not want to duplicate this so much? + action.setTextEquivalent(currentAnnotationValue); + + // TODO: Link this to the RelatedArtifact for References + if (referenceColumnIndex >= 0) { + cell = row.getCell(referenceColumnIndex); + if (cell != null) { + // TODO: Should this be set to the reference from the previous line? + String referenceValue = cell.getStringCellValue(); + RelatedArtifact relatedArtifact = new RelatedArtifact(); + relatedArtifact.setType(RelatedArtifact.RelatedArtifactType.CITATION); + relatedArtifact.setLabel(referenceValue); + action.getDocumentation().add(relatedArtifact); + } + } + + return action; + } + + return null; + } + + // Returns true if the given actions are equal (i.e. are for the same thing, meaning they have the same title, textEquivalent, description, and subactions) + private boolean actionsEqual(PlanDefinition.PlanDefinitionActionComponent currentAction, PlanDefinition.PlanDefinitionActionComponent newAction) { + if (currentAction == null) { + return false; + } + + List<PlanDefinition.PlanDefinitionActionComponent> currentActionSubs = currentAction.getAction(); + List<PlanDefinition.PlanDefinitionActionComponent> newActionSubs = newAction.getAction(); + + + String currentActionDescription = currentActionSubs.stream().map(PlanDefinition.PlanDefinitionActionComponent::getTitle) + .collect(Collectors.joining(" AND ")); + String newActionDescription = newActionSubs.stream().map(PlanDefinition.PlanDefinitionActionComponent::getTitle) + .collect(Collectors.joining(" AND ")); + + return stringsEqual(currentAction.getTitle(), newAction.getTitle()) + && stringsEqual(currentAction.getTextEquivalent(), newAction.getTextEquivalent()) + && stringsEqual(currentActionDescription, newActionDescription) + && subActionsEqual(currentAction.getAction(), newAction.getAction()); + } + + // Merge action conditions as an Or, given that the actions are equal + private void mergeActions(PlanDefinition.PlanDefinitionActionComponent currentAction, + PlanDefinition.PlanDefinitionActionComponent newAction) { + PlanDefinition.PlanDefinitionActionConditionComponent currentCondition = currentAction.getConditionFirstRep(); + PlanDefinition.PlanDefinitionActionConditionComponent newCondition = newAction.getConditionFirstRep(); + + if (currentCondition == null) { + currentAction.getCondition().add(newCondition); + } + else if (newCondition != null) { + currentCondition.getExpression().setDescription(String.format("(%s)%n OR (%s)", + currentCondition.getExpression().getDescription(), newCondition.getExpression().getDescription())); + } + } + + private void writePlanDefinitions(String outputPath) { + if (planDefinitions != null && planDefinitions.size() > 0) { + for (PlanDefinition planDefinition : planDefinitions.values()) { + String outputFilePath = outputPath + File.separator + "input" + File.separator + "resources" + + File.separator + "plandefinition"; + ensurePath(outputFilePath); + writeResource(outputFilePath, planDefinition); + } + } + } + + public void writePlanDefinitionIndex(String outputPath) { + String outputFilePath = outputPath + File.separator + "input" + File.separator + "pagecontent"+ + File.separator + "PlanDefinitionIndex.md"; + ensurePath(outputFilePath); + + try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { + writer.write(buildPlanDefinitionIndex().getBytes()); + writer.flush(); + } + catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing plandefinition index"); + } + } + + private void writeLibraries(String outputPath) { + if (libraries.size() > 0) { + String outputFilePath = outputPath + File.separator + "input" + File.separator + "resources" + + File.separator + "library"; + ensurePath(outputFilePath); + + for (Library library : libraries.values()) { + writeResource(outputFilePath, library); + } + } + } + + private void writeLibraryCQL(String outputPath) { + if (libraryCQL.size() > 0) { + for (Map.Entry<String, StringBuilder> entry : libraryCQL.entrySet()) { + String outputDirectoryPath = outputPath + File.separator + "input" + File.separator + "cql"; + String outputFilePath = outputDirectoryPath + File.separator + entry.getKey() + ".cql"; + ensurePath(outputDirectoryPath); + + try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { + writer.write(entry.getValue().toString().getBytes()); + writer.flush(); + } + catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing CQL: " + entry.getKey()); + } + } + } + } + + private void writeLibraryHeader(StringBuilder cql, Library library) { + cql.append("library ").append(library.getName()); + cql.append(newLine).append(newLine); + cql.append("using FHIR version '4.0.1'"); + cql.append(newLine).append(newLine); + cql.append("include FHIRHelpers version '4.0.1'"); + cql.append(newLine).append(newLine); + cql.append("include ANCConfig called Config"); + cql.append(newLine); + cql.append("include ANCConcepts called Cx"); + cql.append(newLine); + cql.append("include ANCDataElements called PatientData"); + cql.append(newLine).append(newLine); + cql.append("context Patient"); + cql.append(newLine).append(newLine); + } + + private void writeActionCondition(StringBuilder cql, PlanDefinition.PlanDefinitionActionComponent action) { + PlanDefinition.PlanDefinitionActionConditionComponent condition = action.getConditionFirstRep(); + if (condition.getExpression().getExpression() == null) { + condition.getExpression().setExpression( + String.format("Should %s", action.hasDescription() + ? action.getDescription().replace("\"", "\\\"") : "perform action")); + } + cql.append("/*"); + cql.append(newLine); + cql.append(action.getConditionFirstRep().getExpression().getDescription()); + cql.append(newLine); + cql.append("*/"); + cql.append(newLine); + cql.append(String.format("define \"%s\":%n", action.getConditionFirstRep().getExpression().getExpression())); + cql.append(" false"); // Output false, manual process to convert the pseudo-code to CQL + cql.append(newLine); + cql.append(newLine); + } + + private boolean rowIsValid(Row row, int inputColumnIndex, int actionColumnIndex, int annotationColumnIndex) { + // Currently considered "valid" if any of the four known columns have a non-null, non-empty string value. + int[] valueColumnIndexes = new int[] { inputColumnIndex, actionColumnIndex, annotationColumnIndex }; + + for (int i=0; i < valueColumnIndexes.length - 1; i++) { + int columnIndex = valueColumnIndexes[i]; + Cell cell = row.getCell(columnIndex); + if (cell != null) { + String columnValueString = cell.getStringCellValue(); + if (columnValueString == null || columnValueString.isEmpty()) { + return false; + } + } else { + return false; + } + + } + + return true; + } + + private boolean stringsEqual(String left, String right) { + return (left == null && right == null) || (left != null && left.equals(right)); + } + + private boolean subActionsEqual(List<PlanDefinition.PlanDefinitionActionComponent> left, + List<PlanDefinition.PlanDefinitionActionComponent> right) { + if (left == null && right == null) { + return true; + } + + if (left != null && right != null) { + for (int leftIndex = 0; leftIndex < left.size(); leftIndex++) { + if (leftIndex >= right.size()) { + return false; + } + if (!actionsEqual(left.get(leftIndex), right.get(leftIndex))) { + return false; + } + } + return true; + } + // One has a list, the other doesn't + return false; + } + + /* Write Methods */ + public void writeResource(String path, Resource resource) { + String outputFilePath = path + File.separator + resource.getResourceType().toString().toLowerCase() + + "-" + resource.getIdElement().getIdPart() + "." + encoding; + try (FileOutputStream writer = new FileOutputStream(outputFilePath)) { + writer.write( + encoding.equals("json") + ? FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString( + resource).getBytes() + : FhirContext.forR4Cached().newXmlParser().setPrettyPrint(true).encodeResourceToString( + resource).getBytes() + ); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("Error writing resource: " + resource.getIdElement().getIdPart()); + } + } + + private String buildPlanDefinitionIndex() { + StringBuilder index = new StringBuilder().append("### Plan Definitions by Decision ID"); + index.append(newLine).append(newLine); + index.append("|Decision Table|Description|"); + index.append(newLine).append("|---|---|").append(newLine); + + for (PlanDefinition pd : planDefinitions.values()) { + index.append(String.format("|[%s](PlanDefinition-%s.html)|%s|", + pd.getTitle(), pd.getId(), pd.getDescription())); + index.append(newLine); + } + + return index.toString(); + } + + public String getPathToSpreadsheet() { + return pathToSpreadsheet; + } + + public void setPathToSpreadsheet(String pathToSpreadsheet) { + this.pathToSpreadsheet = pathToSpreadsheet; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getDecisionTablePages() { + return decisionTablePages; + } + + public void setDecisionTablePages(String decisionTablePages) { + this.decisionTablePages = decisionTablePages; + } + + public String getDecisionTablePagePrefix() { + return decisionTablePagePrefix; + } + + public void setDecisionTablePagePrefix(String decisionTablePagePrefix) { + this.decisionTablePagePrefix = decisionTablePagePrefix; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public boolean isNumericIdAllowed() { + return Boolean.parseBoolean(numericIdAllowed); + } + + public void setNumericIdAllowed(String numericIdAllowed) { + this.numericIdAllowed = numericIdAllowed; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/README.md new file mode 100644 index 000000000..b30fc4147 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/acceleratorkit/README.md @@ -0,0 +1,21 @@ +## ProcessAcceleratorKit + +The ProcessAcceleratorKit function of [CQF Tooling](https://github.com/cqframework/cqf-tooling) will +read in and process a Data Dictionary that is in the form of a spreadsheet - in a particular/expected +format - and generate the Profile, Extensions and vocabulary (i.e., CodeSystem and ValueSet) resources +for the data elements defined in the data dictionary. + +The \_processDataDictionary script is configured to invoke that functionality. To do that, +follow the steps below. + +### To process a Data Dictionary: + +1. Copy Data Dictionary spreadsheet to this directory (./input/l2). +2. Modify the ./scripts/\_processDataDictionary.sh script + 1. Change the datadictionary_filename value to the name of your data dictionary file, including the extension, or rename your file as DD.xlsx. + 2. Change the datadictionary_sheetname value to the name of the sheet in your data dictionary that you want to process or rename that sheet as "Master-1.0”. + 3. Change the scope variable to the set of scope(s) (comma-delimited list) in the Data Dictionary that you would like to process - this is specified in the ‘Scope’ column of the expected Data Dictionary format. The set of scopes currently registered/supported: Core, ANC, FP, STI, CR +3. Run the \_updateCQFTooling script to get the latest CQF Tooling jar. +4. Run the \_processDataDictionary script to process the Data Dictionary file. + This should produce the profiles, extensions and vocabulary defined in the + data dictionary and output the results to the respective folders in the IG. \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleResources.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleResources.java new file mode 100644 index 000000000..083c3b0e9 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleResources.java @@ -0,0 +1,126 @@ +package org.opencds.cqf.tooling.operations.bundle; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.util.BundleBuilder; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.BundleUtils; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.UUID; + +@Operation(name = "BundleResources") +public class BundleResources implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(BundleResources.class); + @OperationParam(alias = { "ptr", "pathtoresources" }, setter = "setPathToResources", required = true, + description = "Path to the directory containing the resource files to be consolidated into the new bundle (required)") + private String pathToResources; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting Bundle { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "t", "type" }, setter = "setType", defaultValue = "transaction", + description = "The Bundle type as defined in the FHIR specification for the Bundle.type element (default transaction)") + private String type; + @OperationParam(alias = { "bid", "bundleid" }, setter = "setBundleId", + description = "A valid FHIR ID to be used as the ID for the resulting FHIR Bundle (optional)") + private String bundleId; + @OperationParam(alias = { "op", "outputPath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/bundle/output", + description = "The directory path to which the generated Bundle file should be written (default src/main/resources/org/opencds/cqf/tooling/bundle/output)") + private String outputPath; + + @Override + public void execute() { + FhirContext context = FhirContextCache.getContext(version); + BundleTypeEnum bundleType = BundleUtils.getBundleType(type); + if (bundleType == null) { + logger.error("Invalid bundle type: {}", type); + } + else { + IBaseBundle bundle = bundleResources(context, bundleId, bundleType, + IOUtils.readResources(IOUtils.getFilePaths(pathToResources, true), context)); + IOUtils.writeResource(bundle, outputPath == null ? pathToResources : outputPath, + IOUtils.Encoding.parse(encoding), context); + } + } + + public static IBaseBundle bundleResources(@Nonnull FhirContext fhirContext, + String bundleId, BundleTypeEnum type, + @Nonnull List<IBaseResource> resourcesToBundle) { + BundleBuilder builder = new BundleBuilder(fhirContext); + if (type == BundleTypeEnum.COLLECTION) { + builder.setType(type.getCode()); + resourcesToBundle.forEach(builder::addCollectionEntry); + } + else { + builder.setType("transaction"); + resourcesToBundle.forEach(builder::addTransactionUpdateEntry); + } + IBaseBundle bundle = builder.getBundle(); + bundleId = bundleId == null ? UUID.randomUUID().toString() : bundleId; + IDUtils.validateId(bundleId, false); + bundle.setId(bundleId); + return bundle; + } + + public String getPathToResources() { + return pathToResources; + } + + public void setPathToResources(String pathToResources) { + this.pathToResources = pathToResources; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputDirectory) { + this.outputPath = outputDirectory; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleToResources.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleToResources.java new file mode 100644 index 000000000..715ff07d5 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleToResources.java @@ -0,0 +1,84 @@ +package org.opencds.cqf.tooling.operations.bundle; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.BundleUtil; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; + +import javax.annotation.Nonnull; +import java.util.List; + +@Operation(name = "BundleToResources") +public class BundleToResources implements ExecutableOperation { + @OperationParam(alias = { "ptb", "pathtobundle" }, setter = "setPathToBundle", required = true, + description = "Path to the bundle to decompose (required)") + private String pathToBundle; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting resources { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputPath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/bundle/output", + description = "The directory path to which the resource files should be written (default src/main/resources/org/opencds/cqf/tooling/bundle/output)") + private String outputPath; + + @Override + public void execute() { + FhirContext context = FhirContextCache.getContext(version); + IBaseResource possibleBundle = IOUtils.readResource(pathToBundle, context, true); + if (possibleBundle == null) { + throw new IllegalArgumentException("Could not find Bundle at path: " + pathToBundle); + } + if (possibleBundle instanceof IBaseBundle) { + IOUtils.writeResources(bundleToResources(context, (IBaseBundle) possibleBundle), + outputPath == null ? pathToBundle : outputPath, + IOUtils.Encoding.parse(encoding), context); + } + else { + throw new IllegalArgumentException("Expected a Bundle, found " + possibleBundle.fhirType()); + } + } + + public static List<IBaseResource> bundleToResources(@Nonnull FhirContext fhirContext, @Nonnull IBaseBundle bundle) { + return BundleUtil.toListOfResources(fhirContext, bundle); + } + + public String getPathToBundle() { + return pathToBundle; + } + + public void setPathToBundle(String pathToBundle) { + this.pathToBundle = pathToBundle; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputDirectory) { + this.outputPath = outputDirectory; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleTransaction.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleTransaction.java new file mode 100644 index 000000000..d13a1856c --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/BundleTransaction.java @@ -0,0 +1,82 @@ +package org.opencds.cqf.tooling.operations.bundle; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +@Operation(name = "BundleTransaction") +public class BundleTransaction implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(BundleTransaction.class); + @OperationParam(alias = { "ptb", "pathtobundles" }, setter = "setPathToBundles", required = true, + description = "Path to the bundles to load into the FHIR server (required)") + private String pathToBundles; + @OperationParam(alias = { "fs", "fhirServer" }, setter = "setFhirServer", required = true, + description = "The FHIR server where the $transaction operation is executed (required)") + private String fhirServer; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + + @Override + public void execute() { + FhirContext context = FhirContextCache.getContext(version); + List<IBaseBundle> bundles = IOUtils.readResources( + IOUtils.getFilePaths(pathToBundles, true), context) + .stream().filter(IBaseBundle.class::isInstance) + .map(IBaseBundle.class::cast).collect(Collectors.toList()); + bundleTransaction(bundles, context, fhirServer); + } + + public static List<IBaseBundle> bundleTransaction(@Nonnull List<IBaseBundle> bundles, + @Nonnull FhirContext fhirContext, + @Nonnull String fhirServerUri) { + IGenericClient client = fhirContext.newRestfulGenericClient(fhirServerUri); + AtomicReference<IBaseBundle> response = new AtomicReference<>(); + List<IBaseBundle> responseBundles = new ArrayList<>(); + bundles.forEach( + bundle -> { + response.set(client.transaction().withBundle(bundle).execute()); + responseBundles.add(response.get()); + logger.info(IOUtils.encodeResourceAsString(response.get(), IOUtils.Encoding.JSON, fhirContext)); + } + ); + return responseBundles; + } + + public String getPathToBundles() { + return pathToBundles; + } + + public void setPathToBundles(String pathToBundles) { + this.pathToBundles = pathToBundles; + } + + public String getFhirServer() { + return fhirServer; + } + + public void setFhirServer(String fhirServer) { + this.fhirServer = fhirServer; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/README.md new file mode 100644 index 000000000..28a554db0 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/bundle/README.md @@ -0,0 +1,51 @@ +# Bundle Operations + +The operations defined in this package provide support for composing, decomposing, and loading FHIR Bundle resources. + +## BundleResources Operation + +This operation consolidates all resources from files in the 'pathtodirectory' directory into a single FHIR Bundle with +an ID that is the value specified in the 'bunldeid' argument and outputs that generated Bundle in file format of the +type specified by the 'encoding' argument to the 'outputpath' directory. + +### Arguments: +- -pathtodirectory | -ptd (required) - Path to the directory containing the resource files to be consolidated into +the new bundle +- -outputpath | -op (optional) - The directory path to which the generated Bundle file should be written + - Default output path: src/main/resources/org/opencds/cqf/tooling/bundle/output +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting Bundle { json, xml } + - Default encoding: json +- -type | -t (optional) - The Bundle type as defined in the FHIR specification for the Bundle.type element + - Default type: transaction +- -bundleid | -bid (optional) - A valid FHIR ID to be used as the ID for the resulting FHIR Bundle. The Publisher +validation for a Bundle requires a Bundle to have an ID. If no ID is provided, the output Bundle will not have an ID value. + +## BundleToResources Operation + +This operation decomposes a Bundle entry into separate FHIR resource files. + +### Arguments: +- -pathtobundle | -ptb (required) - Path to the bundle to decompose +- -outputpath | -op (optional) - The directory path to which the resource files should be written + - Default output path: src/main/resources/org/opencds/cqf/tooling/bundle/output +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting resources { json, xml } + - Default encoding: json + +## BundleTransaction Operation + +This operation performs the $transaction operation for a directory of FHIR Bundle resources on a specified FHIR server. +TODO: add authentication args + +### Arguments: +- -pathtobundles | -ptb (required) - Path to the bundles to load into the FHIR server +- -fhirserver | -fs (required) - The FHIR server where the $transaction operation is executed +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 + +## BundlesToBundle Operation + +TODO \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/HierarchyProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/HierarchyProcessor.java new file mode 100644 index 000000000..e759f7909 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/HierarchyProcessor.java @@ -0,0 +1,180 @@ +package org.opencds.cqf.tooling.operations.codesystem.loinc; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.ValueSet; +import org.opencds.cqf.tooling.constants.Api; +import org.opencds.cqf.tooling.constants.Terminology; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.HttpClientUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +@Operation(name = "LoincHierarchy") +public class HierarchyProcessor implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(HierarchyProcessor.class); + + @OperationParam(alias = { "query", "q" }, setter = "setQuery", required = true, + description = "The expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g. instructions that could only be followed by a human.") + private String query; + @OperationParam(alias = { "username", "user" }, setter = "setUsername", required = true, + description = "The LOINC account username.") + private String username; + @OperationParam(alias = { "password", "pass" }, setter = "setPassword", required = true, + description = "The LOINC account password.") + private String password; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR ValueSet { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/terminology/output", + description = "The directory path to which the generated FHIR ValueSet resource should be written (default src/main/resources/org/opencds/cqf/tooling/terminology/output)") + private String outputPath; + + private final List<String> validLoincVersions = Arrays.asList("2.69", "2.70", "2.71", "2.72", "2.73", "2.74", "2.75"); + // TODO: need a way to retrieve the latest LOINC version programmatically + private String loincVersion = "2.75"; + + private String loincHierarchyUrl = Api.LOINC_HIERARCHY_QUERY_URL; + + private FhirContext fhirContext; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + IOUtils.writeResource(getValueSet(), outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } + + public IBaseResource getValueSet() { + ValueSet valueSet = new ValueSet().setStatus(Enumerations.PublicationStatus.DRAFT); + valueSet.getCompose().addInclude().setSystem(Terminology.LOINC_SYSTEM_URL).setVersion(loincVersion); + + HttpGet request = new HttpGet(loincHierarchyUrl + URLEncoder.encode(query, Charset.defaultCharset())); + final String auth = username + ":" + password; + final byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.ISO_8859_1)); + final String authHeader = "Basic " + new String(encodedAuth); + request.setHeader(HttpHeaders.AUTHORIZATION, authHeader); + try (CloseableHttpClient queryClient = HttpClients.createDefault()) { + String response = queryClient.execute(request, HttpClientUtils.getDefaultResponseHandler()); + JsonArray arr = new Gson().fromJson(response, JsonArray.class); + for (var obj : arr) { + if (obj.isJsonObject()) { + var jsonObj = obj.getAsJsonObject(); + if (jsonObj.has("IsLoinc") && jsonObj.getAsJsonPrimitive("IsLoinc").getAsBoolean()) { + String code = jsonObj.getAsJsonPrimitive("Code").getAsString(); + String display = null; + if (display == null) { + display = jsonObj.getAsJsonPrimitive("CodeText").getAsString(); + } + valueSet.getCompose().getIncludeFirstRep().addConcept().setCode(code).setDisplay(display); + } + } + } + } catch (IOException ioe) { + String message = "Error accessing API: " + ioe.getMessage(); + logger.error(message); + throw new RuntimeException(message); + } + + return ResourceAndTypeConverter.convertFromR5Resource(fhirContext, valueSet); + } + + private IGenericClient loincFhirClient; + private IGenericClient getLoincFhirClient() { + if (loincFhirClient == null) { + loincFhirClient = fhirContext.newRestfulGenericClient(Api.LOINC_FHIR_SERVER_URL); + loincFhirClient.getInterceptorService().unregisterAllInterceptors(); + loincFhirClient.registerInterceptor(new BasicAuthInterceptor(username, password)); + } + return loincFhirClient; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setLoincHierarchyUrl(String loincHierarchyUrl) { + this.loincHierarchyUrl = loincHierarchyUrl; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public void setLoincVersion(String loincVersion) { + if (validLoincVersions.contains(loincVersion)) { + this.loincVersion = loincVersion; + } else { + logger.warn("Provided LOINC version {} is not supported. Valid versions include: {}", loincVersion, validLoincVersions); + } + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/README.md new file mode 100644 index 000000000..e6f7d40d1 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/loinc/README.md @@ -0,0 +1,18 @@ +# LoincHierarchy Operation + +The purpose of this operation is to generate a simple FHIR ValueSet resource given a query, username, and password using +the LOINC Hierarchy API (https://loinc.org/tree/). + +The returned ValueSet resource will be very simple; including either the rules text (query) as an extension or compose +filters and the resulting API call and post-processed codes in the ValueSet.compose element. + +## Arguments: +- -query | -q (required) - The expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g. instructions that could only be followed by a human. +- -username | -user (required) - The LOINC account username. +- -password | -pass (required) - The LOINC account password. +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR ValueSet resource { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the resulting FHIR ValueSet resource should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/terminology/output \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/README.md new file mode 100644 index 000000000..51091b3cf --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/README.md @@ -0,0 +1,20 @@ +# RxMixWorkflow Operation + +The purpose of this operation is to generate a simple FHIR ValueSet resource given a text narrative (rules text), XML +workflow library, input values, include and exclude filters using the RxMix API (https://mor.nlm.nih.gov/RxMix/). + +The returned ValueSet resource will be very simple; including the rules text as an extension and the resulting API call +and post-processed codes in the ValueSet.compose element. + +## Arguments: +- -rulestext | -rt (required) - An expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g. instructions that could only be followed by a human. +- -workflow | -wf (required) - The workflow library expressed in XML that identifies that API functions needed to produce the desired output. +- -input | -in (required) - The input values needed to run the workflow as a comma-delimited list of strings if multiple inputs are needed. +- -includefilter | -if (required) - The filter(s) that must be present within the RXCUI names for inclusion in the final result. Provide a comma-delimited list of strings for multiple filters. +- -excludefilter | -ef (required) - The filter(s) that must not be present within the RXCUI names for inclusion in the final result. Provide a comma-delimited list of strings for multiple filters. +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR ValueSet resource { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the resulting FHIR ValueSet resource should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/terminology/output \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/RxMixWorkflowProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/RxMixWorkflowProcessor.java new file mode 100644 index 000000000..dc49f94dd --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/codesystem/rxnorm/RxMixWorkflowProcessor.java @@ -0,0 +1,241 @@ +package org.opencds.cqf.tooling.operations.codesystem.rxnorm; + +import ca.uhn.fhir.context.FhirContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Consts; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.ValueSet; +import org.opencds.cqf.tooling.constants.Api; +import org.opencds.cqf.tooling.constants.Terminology; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.HttpClientUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Operation(name = "RxMixWorkflow") +public class RxMixWorkflowProcessor implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(RxMixWorkflowProcessor.class); + + @OperationParam(alias = { "rulestext", "rt" }, setter = "setRulesText", required = true, + description = "An expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g. instructions that could only be followed by a human.") + private String rulesText; + @OperationParam(alias = { "workflow", "wf" }, setter = "setWorkflow", required = true, + description = "The workflow library expressed in XML that identifies that API functions needed to produce the desired output.") + private String workflow; + @OperationParam(alias = { "input", "in" }, setter = "setInput", required = true, + description = "The input values needed to run the workflow as a comma-delimited list of strings if multiple inputs are needed.") + private String input; + private List<String> inputs; + @OperationParam(alias = { "includefilter", "if" }, setter = "setIncludeFilter", + description = "The filter(s) that must be present within the RXCUI names for inclusion in the final result. Provide a comma-delimited list of strings for multiple filters.") + private String includeFilter; + private List<String> includeFilters; + @OperationParam(alias = { "excludefilter", "ef" }, setter = "setExcludeFilter", + description = "The filter(s) that must not be present within the RXCUI names for inclusion in the final result. Provide a comma-delimited list of strings for multiple filters.") + private String excludeFilter; + private List<String> excludeFilters; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR ValueSet { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/terminology/output", + description = "The directory path to which the generated FHIR ValueSet resource should be written (default src/main/resources/org/opencds/cqf/tooling/terminology/output)") + private String outputPath; + + private final LocalDate date = LocalDate.now(); + private String rxNormVersion = String.format("%s-%s", date.getYear(), date.getMonthValue() < 10 ? "0" + date.getMonthValue() : date.getMonthValue()); + + private FhirContext fhirContext; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + IOUtils.writeResource(getValueSet(), outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } + + public IBaseResource getValueSet() { + List<HttpPost> requests = new ArrayList<>(); + for (String inputValue : getInputs()) { + HttpPost request = new HttpPost(Api.RXMIX_WORKFLOW_URL); + request.setEntity(resolveForm(workflow, inputValue)); + requests.add(request); + } + try (CloseableHttpClient client = HttpClients.createDefault()) { + ValueSet vs = new ValueSet().setStatus(Enumerations.PublicationStatus.DRAFT); + vs.addExtension(Terminology.RULES_TEXT_EXT_URL, new StringType(rulesText)); + for (HttpPost request : requests) { + String response = client.execute(request, HttpClientUtils.getDefaultResponseHandler()); + populateValueSet(response, vs); + } + return ResourceAndTypeConverter.convertFromR5Resource(fhirContext, vs); + } catch (IOException ioe) { + String message = "Error accessing RxMix API: " + ioe.getMessage(); + logger.error(message); + throw new RuntimeException(message); + } + } + + private void populateValueSet(String rawResponse, ValueSet vs) { + if (!vs.getCompose().hasInclude()) { + vs.getCompose().addInclude().setSystem(Terminology.RXNORM_SYSTEM_URL).setVersion(rxNormVersion); + } + String[] names = rawResponse.split("\\|name\\|"); + for (String s : names) { + if (!s.startsWith("{\"")) { + String[] rxcuis = s.split("\\|RXCUI\\|"); + String description = rxcuis[0]; + if (!containsExcludeFilter(description) && containsIncludeFilter(description)) { + String code = rxcuis[1].split("\\D")[0]; + vs.getCompose().getIncludeFirstRep().addConcept().setCode(code).setDisplay(description); + } + } + } + } + + private boolean containsExcludeFilter(String s) { + if (getExcludeFilters() != null) { + for (String exclude : excludeFilters) { + if (StringUtils.containsIgnoreCase(s, exclude)) return true; + } + } + return false; + } + + private boolean containsIncludeFilter(String s) { + if (getIncludeFilters() != null) { + for (String include : includeFilters) { + if (StringUtils.containsIgnoreCase(s, include)) return true; + } + return false; + } + return true; + } + + private UrlEncodedFormEntity resolveForm(String xml, String input) { + List<NameValuePair> formparams = new ArrayList<>(); + formparams.add(new BasicNameValuePair("xmlConfig", xml)); + formparams.add(new BasicNameValuePair("inputs", input)); + formparams.add(new BasicNameValuePair("outFormat", "txt")); + return new UrlEncodedFormEntity(formparams, Consts.UTF_8); + } + + public void setRxNormVersion(String rxNormVersion) { + this.rxNormVersion = rxNormVersion; + } + + public String getRulesText() { + return rulesText; + } + + public String getWorkflow() { + return workflow; + } + + public void setWorkflow(String workflow) { + this.workflow = workflow; + } + + public List<String> getInputs() { + if (inputs == null) { + inputs = Arrays.stream(input.split(",")).map(String::trim).collect(Collectors.toList()); + } + return inputs; + } + + public void setInputs(List<String> inputs) { + this.inputs = inputs; + } + + public void setInput(String input) { + this.input = input; + } + + public List<String> getIncludeFilters() { + if (includeFilters == null && includeFilter != null) { + includeFilters = Arrays.stream(includeFilter.split(",")).map(String::trim).collect(Collectors.toList()); + } + return includeFilters; + } + + public void setIncludeFilters(List<String> includeFilters) { + this.includeFilters = includeFilters; + } + + public void setIncludeFilter(String includeFilter) { + this.includeFilter = includeFilter; + } + + public List<String> getExcludeFilters() { + if (excludeFilters == null && excludeFilter != null) { + excludeFilters = Arrays.stream(excludeFilter.split(",")).map(String::trim).collect(Collectors.toList()); + } + return excludeFilters; + } + + public void setExcludeFilters(List<String> excludeFilters) { + this.excludeFilters = excludeFilters; + } + + public void setExcludeFilter(String excludeFilter) { + this.excludeFilter = excludeFilter; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public FhirContext getFhirContext() { + return fhirContext; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public void setRulesText(String rulesText) { + this.rulesText = rulesText; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/README.md new file mode 100644 index 000000000..0fd23bad2 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/README.md @@ -0,0 +1,60 @@ +# RollTestDataDates + +This operation takes a file or a directory and updates the date elements in FHIR resources and CDS Hooks requests. +It then overwrites the original files with the updated ones. + +If a resource in a xml or json file has the following extension + + http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller + +and if the current date is greater than the valueDuration set in that extension (i.e. 30 days) that resource will have +its date, period, dateTimeType, etc. fields changed according to the relation of the date in that field to the +dateLastUpdated value in the extension. This also applies to cds hook request test data. If the extension is not +present, that resource is skipped. If the current date is not more than the duration from the lastUpdated date, that +resource is skipped. + +It may be done based on a file name or a directory. +An example command line would be: + + JAVA -jar tooling-cli-2.1.0-SNAPSHOT.jar -RollTestsDataDates -v=r4 -ip="$USER_HOME$/sandbox/rollDate/files/" + +OR + + JAVA -jar tooling-cli-2.1.0-SNAPSHOT.jar -RollTestsDataDates -v=r4 -ip="$USER_HOME$/sandbox/rollDate/files/bundle-example-rec-02-true-make-recommendations.json" + + +Sample extension: + + "extension": [ + { + "url": "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller", + "extension": [ + { + "url": "dateLastUpdated", + "valueDateTime": "2022-01-28" + }, + { + "url": "frequency", + "valueDuration": { + "value": 30.0, + "unit": "days", + "system": "http://unitsofmeasure.org", + "code": "d" + } + } + ] + } + ], + +### Arguments: +- -pathtoresources (required if -ptreq not present) | -ptres - Path to the directory containing the resource files to +be updated +- -pathtorequests (required if -ptres not present) | -ptreq - Path to the directory containing the CDS Hooks request +files to be updated +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting resource { json, xml } + - Default encoding: json + - CDS Hooks request encoding is JSON - any other values ignored +- -outputpath | -op (optional) - The file system location where the resulting resources/requests are written + - Default path: same as -ptreq or -ptres \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/RollTestDates.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/RollTestDates.java new file mode 100644 index 000000000..1e73b1bf8 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/dateroller/RollTestDates.java @@ -0,0 +1,399 @@ +package org.opencds.cqf.tooling.operations.dateroller; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeChildChoiceDefinition; +import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition; +import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.TerserUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Operation(name = "RollTestDates") +public class RollTestDates implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(RollTestDates.class); + public static final String DATEROLLER_EXT_URL = "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller"; + @OperationParam(alias = { "ptres", "pathtoresources" }, setter = "setPathToResources", + description = "Path to the directory containing the resource files to be updated (required if -ptreq not present)") + private String pathToResources; + @OperationParam(alias = { "ptreq", "pathtorequests" }, setter = "setPathToRequests", + description = "Path to the directory containing the CDS Hooks request files to be updated (required if -ptres not present)") + private String pathToRequests; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting resource { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputPath" }, setter = "setOutputPath", + description = "The file system location where the resulting resources/requests are written (default same as -ptreq or -ptres)") + private String outputPath; + + private FhirContext fhirContext; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + if (pathToResources == null && pathToRequests == null) { + throw new InvalidOperationArgs("Either pathtoresources (-ptres) or pathtorequests (-ptreq) parameter must be provided"); + } + if (pathToResources != null) { + if (outputPath == null) { + outputPath = pathToResources; + } + List<IBaseResource> resources = IOUtils.readResources(Collections.singletonList(pathToResources), fhirContext) + .stream().filter(resource -> ExtensionUtil.hasExtension(resource, DATEROLLER_EXT_URL)) + .filter(resource -> getAllDateElements(fhirContext, resource, getDateClasses(fhirContext))) + .collect(Collectors.toList()); + IOUtils.writeResources(resources, outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } + else { + if (outputPath == null) { + outputPath = pathToRequests; + } + Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); + processCDSHooksRequests(new File(pathToRequests), gson); + } + } + + // NOTE: the legacy CDSHooks prefetch format is NOT supported + private void processCDSHooksRequests(File requestDirectory, Gson gson) { + if (requestDirectory.isDirectory()) { + File[] requests = requestDirectory.listFiles(); + if (requests != null) { + for (File nextFile : requests) { + if (nextFile.isDirectory()) { + processCDSHooksRequests(nextFile, gson); + } + processFile(nextFile, gson); + } + } + } + else if (requestDirectory.isFile()) { + processFile(requestDirectory, gson); + } + } + + private void processFile(File file, Gson gson) { + if (file.getName().toLowerCase(Locale.ROOT).endsWith("json")) { + JsonObject request = gson.fromJson(IOUtils.getFileContent(file), JsonObject.class); + getUpdatedRequest(request, gson); + try (FileWriter writer = new FileWriter(file.getAbsolutePath())) { + writer.write(gson.toJson(request)); + } catch (IOException e) { + logger.error("Error writing file {}", file.getName(), e); + } + } + } + + private IBaseBundle updateBundleDates(IBaseBundle bundle) { + BundleBuilder builder = new BundleBuilder(fhirContext); + BundleUtil.toListOfResources(fhirContext, bundle).forEach( + resource -> { + getAllDateElements(fhirContext, resource, getDateClasses(fhirContext)); + builder.addCollectionEntry(resource); + } + ); + return builder.getBundle(); + } + + // Library method + public void getUpdatedRequest(JsonObject request, Gson gson) { + if (request.has("context") && + request.get("context").getAsJsonObject().has("draftOrders")) { + IBaseResource draftOrdersBundle = fhirContext.newJsonParser().parseResource( + request.getAsJsonObject("context").getAsJsonObject("draftOrders").toString()); + if (draftOrdersBundle instanceof IBaseBundle) { + request.getAsJsonObject("context").add("draftOrders", + gson.fromJson(fhirContext.newJsonParser().encodeResourceToString( + updateBundleDates((IBaseBundle) draftOrdersBundle)), JsonObject.class)); + } + } + if (request.has("prefetch")) { + JsonObject prefetchResources = request.getAsJsonObject("prefetch"); + JsonObject updatedPrefetch = new JsonObject(); + for (Map.Entry<String, JsonElement> prefetchElement : prefetchResources.entrySet()) { + if (prefetchElement.getValue().isJsonNull()) { + updatedPrefetch.add(prefetchElement.getKey(), prefetchElement.getValue()); + continue; + } + IBaseResource resource = fhirContext.newJsonParser().parseResource(prefetchElement.getValue().toString()); + if (resource instanceof IBaseBundle) { + updatedPrefetch.add(prefetchElement.getKey(), gson.fromJson(fhirContext.newJsonParser() + .encodeResourceToString(updateBundleDates((IBaseBundle) resource)), + JsonObject.class)); + } + else { + getAllDateElements(fhirContext, resource, getDateClasses(fhirContext)); + updatedPrefetch.add(prefetchElement.getKey(), gson.fromJson( + fhirContext.newJsonParser().encodeResourceToString(resource), JsonObject.class)); + } + } + request.add("prefetch", updatedPrefetch); + } + } + + // Library method + public boolean getAllDateElements(FhirContext fhirContext, IBaseResource resource, List<Class<? extends IBase>> classes) { + FhirTerser terser = new FhirTerser(fhirContext); + if (ExtensionUtil.hasExtension(resource, DATEROLLER_EXT_URL) && doUpdate(resource)) { + terser.visit(resource, (theResource, theElement, thePathToElement, theChildDefinition, theDefinition) -> { + // TODO - handle timing elements + if (theElement.fhirType().equalsIgnoreCase("timing")) { + return; + } + for (Class<? extends IBase> clazz : classes) { + if (clazz.isAssignableFrom(theElement.getClass())) { + // skip extensions and children of composite Date elements (Period) + if (thePathToElement.contains("extension") || + (theChildDefinition instanceof RuntimeChildPrimitiveDatatypeDefinition && + ((RuntimeChildPrimitiveDatatypeDefinition) theChildDefinition).getField() + .getDeclaringClass().getSimpleName().equalsIgnoreCase("period"))) + { + continue; + } + // Resolve choice type path names by type + if (theChildDefinition instanceof RuntimeChildChoiceDefinition) { + String s = theChildDefinition.getChildNameByDatatype(clazz); + thePathToElement.remove(thePathToElement.size() - 1); + thePathToElement.add(s); + } + int daysToAdd = getDaysBetweenDates(getLastUpdatedDate(resource), new Date()); + if (theElement instanceof org.hl7.fhir.dstu3.model.BaseDateTimeType) { + TimeZone timeZone = ((org.hl7.fhir.dstu3.model.BaseDateTimeType) theElement).getTimeZone(); + ((org.hl7.fhir.dstu3.model.BaseDateTimeType) theElement).setValue(DateUtils.addDays( + ((org.hl7.fhir.dstu3.model.BaseDateTimeType) theElement).getValue(), daysToAdd)) + .setTimeZone(timeZone); + } else if (theElement instanceof org.hl7.fhir.r4.model.BaseDateTimeType) { + TimeZone timeZone = ((org.hl7.fhir.r4.model.BaseDateTimeType) theElement).getTimeZone(); + ((org.hl7.fhir.r4.model.BaseDateTimeType) theElement).setValue(DateUtils.addDays( + ((org.hl7.fhir.r4.model.BaseDateTimeType) theElement).getValue(), daysToAdd)) + .setTimeZone(timeZone); + } else if (theElement instanceof org.hl7.fhir.r5.model.BaseDateTimeType) { + TimeZone timeZone = ((org.hl7.fhir.r5.model.BaseDateTimeType) theElement).getTimeZone(); + ((org.hl7.fhir.r5.model.BaseDateTimeType) theElement).setValue(DateUtils.addDays( + ((org.hl7.fhir.r5.model.BaseDateTimeType) theElement).getValue(), daysToAdd)) + .setTimeZone(timeZone); + } else if (theElement instanceof org.hl7.fhir.dstu3.model.Period) { + org.hl7.fhir.dstu3.model.BaseDateTimeType start = terser.getSingleValueOrNull(theElement, + "start", org.hl7.fhir.dstu3.model.BaseDateTimeType.class); + org.hl7.fhir.dstu3.model.BaseDateTimeType end = terser.getSingleValueOrNull(theElement, + "end", org.hl7.fhir.dstu3.model.BaseDateTimeType.class); + ((org.hl7.fhir.dstu3.model.Period) theElement).setEnd(end.setValue( + DateUtils.addDays(end.getValue(), daysToAdd)).setTimeZone(end.getTimeZone()).getValue()); + ((org.hl7.fhir.dstu3.model.Period) theElement).setStart(start.setValue( + DateUtils.addDays(start.getValue(), daysToAdd)).setTimeZone(start.getTimeZone()).getValue()); + } else if (theElement instanceof org.hl7.fhir.r4.model.Period) { + org.hl7.fhir.r4.model.BaseDateTimeType start = terser.getSingleValueOrNull(theElement, + "start", org.hl7.fhir.r4.model.BaseDateTimeType.class); + org.hl7.fhir.r4.model.BaseDateTimeType end = terser.getSingleValueOrNull(theElement, + "end", org.hl7.fhir.r4.model.BaseDateTimeType.class); + ((org.hl7.fhir.r4.model.Period) theElement).setEnd(end.setValue( + DateUtils.addDays(end.getValue(), daysToAdd)).setTimeZone(end.getTimeZone()).getValue()); + ((org.hl7.fhir.r4.model.Period) theElement).setStart(start.setValue( + DateUtils.addDays(start.getValue(), daysToAdd)).setTimeZone(start.getTimeZone()).getValue()); + } else if (theElement instanceof org.hl7.fhir.r5.model.Period) { + org.hl7.fhir.r5.model.BaseDateTimeType start = terser.getSingleValueOrNull(theElement, + "start", org.hl7.fhir.r5.model.BaseDateTimeType.class); + org.hl7.fhir.r5.model.BaseDateTimeType end = terser.getSingleValueOrNull(theElement, + "end", org.hl7.fhir.r5.model.BaseDateTimeType.class); + ((org.hl7.fhir.r5.model.Period) theElement).setEnd(end.setValue( + DateUtils.addDays(end.getValue(), daysToAdd)).setTimeZone(end.getTimeZone()).getValue()); + ((org.hl7.fhir.r5.model.Period) theElement).setStart(start.setValue( + DateUtils.addDays(start.getValue(), daysToAdd)).setTimeZone(start.getTimeZone()).getValue()); + } else { + throw new IllegalArgumentException( + "Expected type: date | datetime | timing | instant | period, found: " + + theElement.fhirType()); + } + TerserUtil.setFieldByFhirPath(terser, resolveFhirPath(theResource.fhirType(), thePathToElement), + resource, theElement); + updateDateRollerExtension(fhirContext, resource); + } + } + }); + return true; + } + return false; + } + + private int getDaysBetweenDates(Date start, Date end) { + long startInMs = start.getTime(); + long endInMs = end.getTime(); + long timeDiff = Math.abs(endInMs - startInMs); + return (int) TimeUnit.DAYS.convert(timeDiff, TimeUnit.MILLISECONDS); + } + + private Date getLastUpdatedDate(IBaseResource resource) { + IBaseDatatype dateLastUpdated = ExtensionUtil.getExtensionByUrl( + ExtensionUtil.getExtensionByUrl(resource, DATEROLLER_EXT_URL), "dateLastUpdated").getValue(); + if (dateLastUpdated instanceof org.hl7.fhir.dstu3.model.BaseDateTimeType) { + return ((org.hl7.fhir.dstu3.model.BaseDateTimeType) dateLastUpdated).getValue(); + } else if (dateLastUpdated instanceof org.hl7.fhir.r4.model.BaseDateTimeType) { + return ((org.hl7.fhir.r4.model.BaseDateTimeType) dateLastUpdated).getValue(); + } else if (dateLastUpdated instanceof org.hl7.fhir.r5.model.BaseDateTimeType) { + return ((org.hl7.fhir.r5.model.BaseDateTimeType) dateLastUpdated).getValue(); + } else { + throw new IllegalArgumentException("Unsupported type/version found for dateLastUpdated extension: " + + dateLastUpdated.fhirType()); + } + } + + private int getFrequencyInDays(IBaseResource resource) { + IBaseDatatype frequency = ExtensionUtil.getExtensionByUrl( + ExtensionUtil.getExtensionByUrl(resource, DATEROLLER_EXT_URL), "frequency").getValue(); + int numDays; + String precision; + if (frequency instanceof org.hl7.fhir.dstu3.model.Duration) { + numDays = ((org.hl7.fhir.dstu3.model.Duration) frequency).getValue().intValue(); + precision = StringUtils.firstNonEmpty(((org.hl7.fhir.dstu3.model.Duration) frequency).getCode(), ((org.hl7.fhir.dstu3.model.Duration) frequency).getUnit()); + } else if (frequency instanceof org.hl7.fhir.r4.model.Duration) { + numDays = ((org.hl7.fhir.r4.model.Duration) frequency).getValue().intValue(); + precision = StringUtils.firstNonEmpty(((org.hl7.fhir.r4.model.Duration) frequency).getCode(), ((org.hl7.fhir.r4.model.Duration) frequency).getUnit()); + } else if (frequency instanceof org.hl7.fhir.r5.model.Duration) { + numDays = ((org.hl7.fhir.r5.model.Duration) frequency).getValue().intValue(); + precision = StringUtils.firstNonEmpty(((org.hl7.fhir.r5.model.Duration) frequency).getCode(), ((org.hl7.fhir.r5.model.Duration) frequency).getUnit()); + } else { + throw new IllegalArgumentException("Unsupported type/version found for frequency duration extension: " + frequency.fhirType()); + } + if (precision == null) { + throw new IllegalArgumentException("The frequency duration precision not found"); + } else if (precision.toLowerCase().startsWith("d")) { + return numDays; + } else if (precision.toLowerCase().startsWith("w")) { + return numDays * 7; + } else if (precision.toLowerCase().startsWith("m")) { + return numDays * 30; + } else if (precision.toLowerCase().startsWith("y")) { + return numDays * 365; + } else { + throw new IllegalArgumentException("The frequency duration precision is invalid. Must be one of (d | w | m | y)"); + } + } + + private boolean doUpdate(IBaseResource resource) { + Date today = new Date(); + return DateUtils.addDays(getLastUpdatedDate(resource), getFrequencyInDays(resource)).before(today); + } + + private void updateDateRollerExtension(FhirContext fhirContext, IBaseResource resource) { + ExtensionUtil.setExtension(fhirContext, ExtensionUtil.getExtensionByUrl(ExtensionUtil.getExtensionByUrl( + resource, DATEROLLER_EXT_URL), "dateLastUpdated"), "dateTime", new Date()); + } + + public List<Class<? extends IBase>> getDateClasses(FhirContext fhirContext) { + List<Class<? extends IBase>> classes = new ArrayList<>(); + if (fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { + Collections.addAll(classes, org.hl7.fhir.dstu3.model.DateTimeType.class, + org.hl7.fhir.dstu3.model.DateType.class, + org.hl7.fhir.dstu3.model.InstantType.class, + org.hl7.fhir.dstu3.model.Timing.class, + org.hl7.fhir.dstu3.model.Period.class); + } else if (fhirContext.getVersion().getVersion() == FhirVersionEnum.R4) { + Collections.addAll(classes, org.hl7.fhir.r4.model.DateTimeType.class, + org.hl7.fhir.r4.model.DateType.class, + org.hl7.fhir.r4.model.InstantType.class, + org.hl7.fhir.r4.model.Timing.class, + org.hl7.fhir.r4.model.Period.class); + } else if (fhirContext.getVersion().getVersion() == FhirVersionEnum.R5) { + Collections.addAll(classes, org.hl7.fhir.r5.model.DateTimeType.class, + org.hl7.fhir.r5.model.DateType.class, + org.hl7.fhir.r5.model.InstantType.class, + org.hl7.fhir.r5.model.Timing.class, + org.hl7.fhir.r5.model.Period.class); + } else { + throw new UnsupportedOperationException("FHIR version " + + fhirContext.getVersion().getVersion().getFhirVersionString() + + " is not supported for this operation."); + } + return classes; + } + + private String resolveFhirPath(String resourceType, List<String> paths) { + StringBuilder builder = new StringBuilder(); + builder.append(resourceType).append("."); + paths.forEach(path -> builder.append(path).append(".")); + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + public String getPathToResources() { + return pathToResources; + } + + public void setPathToResources(String pathToResources) { + this.pathToResources = pathToResources; + } + + public String getPathToRequests() { + return pathToRequests; + } + + public void setPathToRequests(String pathToRequests) { + this.pathToRequests = pathToRequests; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public FhirContext getFhirContext() { + return fhirContext; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/ig/RefreshIG.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/ig/RefreshIG.java new file mode 100644 index 000000000..cba38d2e0 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/ig/RefreshIG.java @@ -0,0 +1,45 @@ +package org.opencds.cqf.tooling.operations.ig; + +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; + +@Operation(name = "RefreshIG") +public class RefreshIG implements ExecutableOperation { + + @OperationParam(alias = { "ip", "igp", "pathtoig" }, setter = "setPathToImplementationGuide", required = true, + description = "Path to the root directory of the Implementation Guide (required).") + private String pathToImplementationGuide; + @OperationParam(alias = { "elm", "pwelm", "packagewithelm" }, setter = "setIncludeElm", defaultValue = "false", + description = "Determines whether ELM will be produced or packaged (omitted by default).") + private Boolean includeElm; + @OperationParam(alias = { "d", "id", "pd", "packagedependencies" }, setter = "setIncludeDependencies", defaultValue = "false", + description = "Determines whether libraries other than the primary will be packaged (omitted by default).") + private Boolean includeDependencies; + @OperationParam(alias = { "t", "it", "pt", "packageterminology" }, setter = "setIncludeTerminology", defaultValue = "false", + description = "Determines whether terminology will be packaged (omitted by default).") + private Boolean includeTerminology; + @OperationParam(alias = { "p", "ipat", "pp", "packagepatients" }, setter = "setIncludePatients", defaultValue = "false", + description = "Determines whether patient scenario information will be packaged (omitted by default).") + private Boolean includePatients; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR Library { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + description = "The directory path to which the generated FHIR resources should be written (default is to replace existing resources within the IG)") + private String outputPath; + + @Override + public void execute() { + // refresh libraries + // package (Bundle or list of resources) + // refresh measures + // package (Bundle or list of resources) + // refresh plandefinitions + // package (Bundle or list of resources) - also includes + // publish + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryGenerator.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryGenerator.java new file mode 100644 index 000000000..237ec317d --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryGenerator.java @@ -0,0 +1,210 @@ +package org.opencds.cqf.tooling.operations.library; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.TerserUtil; +import org.apache.commons.codec.binary.Base64; +import org.cqframework.cql.cql2elm.CqlTranslator; +import org.cqframework.cql.cql2elm.CqlTranslatorOptions; +import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.LibrarySourceProvider; +import org.cqframework.cql.cql2elm.ModelManager; +import org.cqframework.cql.elm.requirements.fhir.DataRequirementsProcessor; +import org.hl7.elm.r1.IncludeDef; +import org.hl7.elm.r1.Library; +import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_50; +import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; +import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50; +import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +@Operation(name = "CqlToLibrary") +public class LibraryGenerator implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(LibraryGenerator.class); + + // TODO: either enable user to pass in translator options as a param or search directory for cql-options.json + @OperationParam(alias = { "ptcql", "pathtocqlcontent" }, setter = "setPathToCqlContent", required = true, + description = "Path to the directory or file containing the CQL content to be transformed into FHIR Library resources (required)") + private String pathToCqlContent; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR Library { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/library/output", + description = "The directory path to which the generated FHIR Library resources should be written (default src/main/resources/org/opencds/cqf/tooling/library/output)") + private String outputPath; + + @OperationParam(alias = {"numid", "numericidallowed"}, setter = "setNumericIdAllowed", defaultValue = "false", + description = "Determines if we want to allow numeric IDs (This overrides default HAPI behaviour") + private String numericIdAllowed; + + private FhirContext fhirContext; + private final DataRequirementsProcessor dataRequirementsProcessor = new DataRequirementsProcessor(); + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + ModelManager modelManager = new ModelManager(); + LibraryManager libraryManager = new LibraryManager(modelManager); + File cqlContent = new File(pathToCqlContent); + LibrarySourceProvider librarySourceProvider = new DefaultLibrarySourceProvider(cqlContent.isDirectory() ? + cqlContent.toPath() : cqlContent.getParentFile().toPath()); + libraryManager.getLibrarySourceLoader().registerProvider(librarySourceProvider); + + if (cqlContent.isDirectory()) { + File[] cqlFiles = cqlContent.listFiles(); + if (cqlFiles != null) { + for (File cqlFile : cqlFiles) { + if (cqlFile.getAbsolutePath().endsWith("cql")) { + processCqlFile(cqlFile, modelManager, libraryManager); + } + } + } + } + else { + processCqlFile(cqlContent, modelManager, libraryManager); + } + } + + private void processCqlFile(File cqlFile, ModelManager modelManager, LibraryManager libraryManager) { + if (cqlFile.getAbsolutePath().endsWith("cql")) { + try { + CqlTranslator translator = CqlTranslator.fromFile(cqlFile, modelManager, libraryManager, + CqlTranslatorOptions.defaultOptions().getOptions().toArray( + new CqlTranslatorOptions.Options[]{})); + org.hl7.fhir.r5.model.Library dataReqLibrary = dataRequirementsProcessor.gatherDataRequirements( + libraryManager, translator.getTranslatedLibrary(), CqlTranslatorOptions.defaultOptions(), + null, false); + IOUtils.writeResource(resolveFhirLibrary(translator, dataReqLibrary, IOUtils.getFileContent(cqlFile)), + outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } catch (IOException e) { + logger.error("Error encountered translating {}", cqlFile.getAbsolutePath(), e); + } + } + } + + // Library access method + public IBaseResource resolveFhirLibrary(CqlTranslator cqlTranslator, org.hl7.fhir.r5.model.Library dataReqLibrary, String cql) { + IBaseResource library; + switch (fhirContext.getVersion().getVersion()) { + case DSTU3: + library = new VersionConvertor_30_50(new BaseAdvisor_30_50()).convertResource(dataReqLibrary); + break; + case R4: + library = new VersionConvertor_40_50(new BaseAdvisor_40_50()).convertResource(dataReqLibrary); + break; + case R5: + library = dataReqLibrary; + break; + default: + throw new UnsupportedOperationException(String.format( + "Library generation for version %s is not supported", + fhirContext.getVersion().getVersion().getFhirVersionString())); + } + + Library elmLibrary = cqlTranslator.toELM(); + + library.setId(IDUtils.libraryNameToId(elmLibrary.getIdentifier().getId(), + elmLibrary.getIdentifier().getVersion(), IsNumericIdAllowed())); + FhirTerser terser = new FhirTerser(fhirContext); + + // basic metadata information + terser.setElement(library, "name", elmLibrary.getIdentifier().getId()); + terser.setElement(library, "version", elmLibrary.getIdentifier().getVersion()); + terser.setElement(library, "experimental", "true"); + terser.setElement(library, "status", "active"); + IBase type = TerserUtil.newElement(fhirContext, "CodeableConcept"); + IBase typeCoding = TerserUtil.newElement(fhirContext, "Coding"); + terser.setElement(typeCoding, "code", "logic-library"); + terser.setElement(typeCoding, "system", "http://hl7.org/fhir/library-type"); + terser.setElement(typeCoding, "display", "Logic Library"); + TerserUtil.setField(fhirContext, "type", library, type); + TerserUtil.setFieldByFhirPath(fhirContext, "Library.type.coding", library, typeCoding); + + // content + IBase cqlAttachment = TerserUtil.newElement(fhirContext, "Attachment"); + terser.setElement(cqlAttachment, "contentType", "text/cql"); + terser.setElement(cqlAttachment, "data", Base64.encodeBase64String(cql.getBytes())); + IBase elmXmlAttachment = TerserUtil.newElement(fhirContext, "Attachment"); + terser.setElement(elmXmlAttachment, "contentType", "application/elm+xml"); + terser.setElement(elmXmlAttachment, "data", Base64.encodeBase64String(cqlTranslator.toXml().getBytes())); + IBase elmJsonAttachment = TerserUtil.newElement(fhirContext, "Attachment"); + terser.setElement(elmJsonAttachment, "contentType", "application/elm+json"); + terser.setElement(elmJsonAttachment, "data", Base64.encodeBase64String(cqlTranslator.toJson().getBytes())); + TerserUtil.setField(fhirContext, "content", library, cqlAttachment, elmXmlAttachment, elmJsonAttachment); + + return library; + } + + private String getIncludedLibraryId(IncludeDef def) { + return IDUtils.libraryNameToId(getIncludedLibraryName(def), def.getVersion(), IsNumericIdAllowed()); + } + + private String getIncludedLibraryName(IncludeDef def) { + return def.getPath(); + } + + public String getPathToCqlContent() { + return pathToCqlContent; + } + + public void setPathToCqlContent(String pathToCqlContent) { + this.pathToCqlContent = pathToCqlContent; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public boolean IsNumericIdAllowed() { + return Boolean.parseBoolean(numericIdAllowed); + } + + public void setNumericIdAllowed(String numericIdAllowed) { + this.numericIdAllowed = numericIdAllowed; + } + + public FhirContext getFhirContext() { + return fhirContext; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryRefresh.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryRefresh.java new file mode 100644 index 000000000..adff5579b --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/LibraryRefresh.java @@ -0,0 +1,187 @@ +package org.opencds.cqf.tooling.operations.library; + +import ca.uhn.fhir.context.FhirContext; +import org.cqframework.cql.cql2elm.CqlTranslator; +import org.cqframework.cql.cql2elm.CqlTranslatorOptions; +import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.ModelManager; +import org.cqframework.cql.cql2elm.quick.FhirLibrarySourceProvider; +import org.cqframework.cql.elm.requirements.fhir.DataRequirementsProcessor; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.Attachment; +import org.hl7.fhir.r5.model.Library; +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.ResourceUtils; +import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Date; + +@Operation(name = "RefreshLibrary") +public class LibraryRefresh implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(LibraryRefresh.class); + + @OperationParam(alias = { "ptl", "pathtolibrary" }, setter = "setPathToLibrary", required = true, + description = "Path to the FHIR Library resource to refresh (required).") + private String pathToLibrary; + @OperationParam(alias = { "ptcql", "pathtocql" }, setter = "setPathToCql", required = true, + description = "Path to the CQL content referenced or depended on by the FHIR Library resource to refresh (required).") + private String pathToCql; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR Library { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + description = "The directory path to which the generated FHIR resources should be written (default is to replace existing resources within the IG)") + private String outputPath; + + private FhirContext fhirContext; + private ModelManager modelManager; + private LibraryManager libraryManager; + private CqlTranslatorOptions translatorOptions = CqlTranslatorOptions.defaultOptions(); + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + + IBaseResource libraryToRefresh = IOUtils.readResource(pathToLibrary, fhirContext); + if (!libraryToRefresh.fhirType().equalsIgnoreCase("library")) { + throw new InvalidOperationArgs("Expected resource of type Library, found " + libraryToRefresh.fhirType()); + } + + try { + modelManager = new ModelManager(); + libraryManager = new LibraryManager(modelManager); + libraryManager.getLibrarySourceLoader().registerProvider(new DefaultLibrarySourceProvider(Paths.get(pathToCql))); + libraryManager.getLibrarySourceLoader().registerProvider(new FhirLibrarySourceProvider()); + translatorOptions = ResourceUtils.getTranslatorOptions(pathToCql); + refreshLibrary(libraryToRefresh); + + if (outputPath == null) { + outputPath = pathToLibrary; + } + + IOUtils.writeResource(libraryToRefresh, outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } catch (Exception e) { + logger.error("Error refreshing library: {}", pathToLibrary, e); + } + } + + // Library access method + public IBaseResource refreshLibrary(IBaseResource libraryToRefresh) { + Library r5LibraryToRefresh = (Library) ResourceAndTypeConverter.convertToR5Resource(fhirContext, libraryToRefresh); + + try { + String cql = new String(libraryManager.getLibrarySourceLoader().getLibrarySource(ResourceUtils.getIdentifier(libraryToRefresh, fhirContext)).readAllBytes()); + CqlTranslator translator = CqlTranslator.fromText(cql, modelManager, libraryManager, null, translatorOptions); + DataRequirementsProcessor drp = new DataRequirementsProcessor(); + Library drLibrary = drp.gatherDataRequirements(libraryManager, translator.getTranslatedLibrary(), translatorOptions, null, true, false); + + r5LibraryToRefresh.setDate(new Date()); + refreshContent(r5LibraryToRefresh, cql, translator.toXml(), translator.toJson()); + r5LibraryToRefresh.setDataRequirement(drLibrary.getDataRequirement()); + r5LibraryToRefresh.setRelatedArtifact(drLibrary.getRelatedArtifact()); + r5LibraryToRefresh.setParameter(drLibrary.getParameter()); + } catch (Exception e) { + logger.error("Error refreshing library: {}", pathToLibrary, e); + return null; + } + + return ResourceAndTypeConverter.convertFromR5Resource(fhirContext, r5LibraryToRefresh); + } + + private String getCqlFromLibrary(Library library) { + for (var content : library.getContent()) { + if (content.hasContentType() && content.getContentType().equalsIgnoreCase("text/cql")) { + return new String(content.getData()); + } + } + return null; + } + + private void refreshContent(Library library, String cql, String elmXml, String elmJson) { + library.setContent(Arrays.asList( + new Attachment().setContentType("text/cql").setData(cql.getBytes()), + new Attachment().setContentType("application/elm+xml").setData(elmXml.getBytes()), + new Attachment().setContentType("application/elm+json").setData(elmJson.getBytes()))); + } + + public String getPathToLibrary() { + return pathToLibrary; + } + + public void setPathToLibrary(String pathToLibrary) { + this.pathToLibrary = pathToLibrary; + } + + public String getPathToCql() { + return pathToCql; + } + + public void setPathToCql(String pathToCql) { + this.pathToCql = pathToCql; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public ModelManager getModelManager() { + return modelManager; + } + + public void setModelManager(ModelManager modelManager) { + this.modelManager = modelManager; + } + + public LibraryManager getLibraryManager() { + return libraryManager; + } + + public void setLibraryManager(LibraryManager libraryManager) { + this.libraryManager = libraryManager; + } + + public CqlTranslatorOptions getTranslatorOptions() { + return translatorOptions; + } + + public void setTranslatorOptions(CqlTranslatorOptions translatorOptions) { + this.translatorOptions = translatorOptions; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/README.md new file mode 100644 index 000000000..1daca5005 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/library/README.md @@ -0,0 +1,44 @@ +# Library Operations + +The operations defined in this package provide support for generating and updating FHIR Library resources. + +## CqlToLibrary Operation + +This operation transforms all CQL content in the 'pathtocqlcontent' directory or file into FHIR Library resources. +The resulting Library resource(s) will contain the following elements if applicable: + +- Library.id - combined named identifier and version associated with the CQL library +- Library.name - named identifier associated with the CQL library +- Library.version - version associated with the CQL library +- Library.experimental - defaulted to true +- Library.status - defaulted to 'active' +- Library.type - defaulted to 'logic-library' +- Library.relatedArtifact - contains the Library dependencies (terminology and other Libraries) +- Library.parameter - contains the parameters defined in the CQL library +- Library.dataRequirement - contains the data requirements for the CQL library +- Library.content - contains the base64 encoded CQL and ELM content for the CQL library + +## Arguments: +- -pathtocqlcontent | -ptcql (required) - Path to the directory or file containing the CQL content to be transformed +into FHIR Library resources. +- -version | -v (optional) - FHIR version { stu3, r4, r5 }. + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR Library { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the generated FHIR Library resources should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/library/output + +## RefreshLibrary Operation + +This operation refreshes a FHIR Library resource content (CQL and ELM), data requirements, related artifacts, and +parameters (in and out). + +## Arguments: +- -pathtolibrary | -ptl (required) - Path to the FHIR Library resource to refresh. +- pathtocql | ptcql (required) - Path to the CQL content referenced or depended on by the FHIR Library resource to refresh. +- -version | -v (optional) - FHIR version { stu3, r4, r5 }. + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR Library { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the generated FHIR Library resources should be written. + - Default output path: same as -pathtolibrary (-ptl) \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundle.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundle.java new file mode 100644 index 000000000..dc1a14561 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundle.java @@ -0,0 +1,287 @@ +package org.opencds.cqf.tooling.operations.mat; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ResourceUtil; +import ca.uhn.fhir.util.TerserUtil; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +@Operation(name = "ExtractMatBundle") +public class ExtractMatBundle implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(ExtractMatBundle.class); + + @OperationParam(alias = { "ptb", "pathtobundle" }, setter = "pathToBundle", required = true, + description = "Path to the exported MAT FHIR Bundle resource (required)") + private String pathToBundle; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting extracted FHIR resources { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "sn", "suppressnarrative" }, setter = "setSuppressNarrative", defaultValue = "true", + description = "Whether or not to suppress Narratives in extracted Measure resources (default true)") + private Boolean suppressNarrative; + @OperationParam(alias = { "op", "outputPath" }, setter = "setOutputPath", + description = "The directory path to which the generated Bundle file should be written (default parent directory of -ptb)") + private String outputPath; + + private FhirContext fhirContext; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + IBaseResource bundle = IOUtils.readResource(pathToBundle, fhirContext); + if (outputPath == null) { + outputPath = new File(pathToBundle).getParent(); + } + if (bundle instanceof IBaseBundle) { + processBundle((IBaseBundle) bundle); + } else if (bundle == null) { + logger.error("Unable to read Bundle resource at {}", pathToBundle); + } else { + String type = bundle.fhirType(); + logger.error("Expected a Bundle resource, found {}", type); + } + } + + public void processBundle(IBaseBundle bundle) { + List<IBaseResource> resources = BundleUtil.toListOfResources(fhirContext, bundle); + createDirectoryStructure(); + migrateResources(resources); + } + + // Library access method + public MatPackage getMatPackage(IBaseBundle bundle) { + MatPackage matPackage = new MatPackage(); + List<IBaseResource> resources = BundleUtil.toListOfResources(fhirContext, bundle); + FhirTerser terser = new FhirTerser(fhirContext); + for (IBaseResource resource : resources) { + if (resource.fhirType().equalsIgnoreCase("measure") + && Boolean.TRUE.equals(suppressNarrative)) { + ResourceUtil.removeNarrative(fhirContext, resource); + } + if (resource.fhirType().equalsIgnoreCase("library")) { + matPackage.addLibraryPackage( + new MatPackage.LibraryPackage().setLibrary(resource).setCql( + new String(Base64.decodeBase64(extractCql(resource, terser))))); + } else if (resource.fhirType().equalsIgnoreCase("measure")) { + matPackage.addMeasure(resource); + } else { + matPackage.addResource(resource); + } + } + return matPackage; + } + + private void migrateResources(List<IBaseResource> resources) { + FhirTerser terser = new FhirTerser(fhirContext); + for (IBaseResource resource : resources) { + if (resource.fhirType().equalsIgnoreCase("measure") + && Boolean.TRUE.equals(suppressNarrative)) { + ResourceUtil.removeNarrative(fhirContext, resource); + } + if (resource.fhirType().equalsIgnoreCase("library")) { + extractLibrary(resource, terser); + } else if (resource.fhirType().equalsIgnoreCase("measure")) { + extractMeasure(resource, terser); + } else { + IOUtils.writeResource(resource, resourcesPath.toString(), IOUtils.Encoding.valueOf(encoding), fhirContext); + } + } + } + + private void extractLibrary(IBaseResource library, FhirTerser terser) { + String name = terser.getSinglePrimitiveValueOrNull(library, "name"); + if (name == null) { + name = library.getIdElement().getIdPart(); + } + IOUtils.writeResource(library, libraryOutputPath.toString(), IOUtils.Encoding.valueOf(encoding), fhirContext, + false, name); + String cql = extractCql(library, terser); + if (cql != null) { + String cqlPath = Paths.get(cqlOutputPath.toString(), name) + ".cql"; + try { + FileUtils.writeByteArrayToFile(new File(cqlPath), Base64.decodeBase64(cql)); + } catch (IOException e) { + logger.warn("Error writing CQL file: {}", cqlPath, e); + } + } else { + logger.warn("Unable to extract CQL from Library: {}", name); + } + } + + private String extractCql(IBaseResource library, FhirTerser terser) { + for (IBase attachment : TerserUtil.getValues(fhirContext, library, "content")) { + String contentType = terser.getSinglePrimitiveValueOrNull(attachment, "contentType"); + if (contentType != null && contentType.equals("text/cql")) { + return terser.getSinglePrimitiveValueOrNull(attachment, "data"); + } + } + return null; + } + + private void extractMeasure(IBaseResource measure, FhirTerser terser) { + String name = terser.getSinglePrimitiveValueOrNull(measure, "name"); + if (name == null) { + name = measure.getIdElement().getIdPart(); + } + IOUtils.writeResource(measure, measureOutputPath.toString(), IOUtils.Encoding.valueOf(encoding), fhirContext, + false, name); + } + + private Path newOutputPath; + private Path resourcesPath; + private Path libraryOutputPath; + private Path measureOutputPath; + private Path cqlOutputPath; + private void createDirectoryStructure() { + String targetDir = "bundles"; + if (!outputPath.contains(targetDir)) { + newOutputPath = Paths.get(outputPath, targetDir, "input"); + } else { + newOutputPath = Paths.get(outputPath.substring(0, outputPath.lastIndexOf(targetDir)), "input"); + } + resourcesPath = Paths.get(newOutputPath.toString(), "resources"); + libraryOutputPath = Paths.get(resourcesPath.toString(), "library"); + measureOutputPath = Paths.get(resourcesPath.toString(), "measure"); + cqlOutputPath = Paths.get(newOutputPath.toString(), "cql"); + + String warningMessage = "Unable to create directory at {}"; + if (!libraryOutputPath.toFile().mkdirs()) { + logger.warn(warningMessage, libraryOutputPath); + } + if (!measureOutputPath.toFile().mkdirs()) { + logger.warn(warningMessage, measureOutputPath); + } + if (!cqlOutputPath.toFile().mkdirs()) { + logger.warn(warningMessage, cqlOutputPath); + } + } + + public String getPathToBundle() { + return pathToBundle; + } + + public void setPathToBundle(String pathToBundle) { + this.pathToBundle = pathToBundle; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Boolean getSuppressNarrative() { + return suppressNarrative; + } + + public void setSuppressNarrative(Boolean suppressNarrative) { + this.suppressNarrative = suppressNarrative; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public FhirContext getFhirContext() { + return fhirContext; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public static class MatPackage { + private final List<LibraryPackage> libraryPackages; + private final List<IBaseResource> measures; + private final List<IBaseResource> otherResources; + + public MatPackage() { + this.libraryPackages = new ArrayList<>(); + this.measures = new ArrayList<>(); + this.otherResources = new ArrayList<>(); + } + + public List<LibraryPackage> getLibraryPackages() { + return libraryPackages; + } + + public void addLibraryPackage(LibraryPackage libraryPackage) { + this.libraryPackages.add(libraryPackage); + } + + public List<IBaseResource> getMeasures() { + return measures; + } + + public void addMeasure(IBaseResource measure) { + this.measures.add(measure); + } + + public List<IBaseResource> getOtherResources() { + return otherResources; + } + + public void addResource(IBaseResource resource) { + this.otherResources.add(resource); + } + + public static class LibraryPackage { + private IBaseResource library; + private String cql; + + public IBaseResource getLibrary() { + return library; + } + + public LibraryPackage setLibrary(IBaseResource library) { + this.library = library; + return this; + } + + public String getCql() { + return cql; + } + + public LibraryPackage setCql(String cql) { + this.cql = cql; + return this; + } + } + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/README.md new file mode 100644 index 000000000..5e77e56e2 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/mat/README.md @@ -0,0 +1,26 @@ +# ExtractMatBundle Operation + +This operation extracts the resources from an exported MAT FHIR Bundle and writes them to the containing directory in +the following format: + +``` +- Parent directory + |- exported MAT FHIR Bundle Resource + |- bundles + |- input + |- resources (contains the library and measure subdirectories as well as any other resources contained in the MAT Bundle) + |- library (contains all FHIR Library resources where Library.name = filename + encoding) + |- measure (contains all FHIR Measure resources where Measure.name = filename + encoding) + |- cql (contains the extracted CQL content from the Library resources where Library.name = filename + '.cql') +``` + +## Arguments: +- -pathtobundle | -ptb (required) - Path to the exported MAT FHIR Bundle resource +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting extracted FHIR resources { json, xml } + - Default encoding: json +- -suppressnarratives | -sn (optional) - Whether or not to suppress Narratives in extracted Measure resources + - Default value: true +- -outputpath | -op (optional) - The directory path to which the resulting extracted FHIR Library resources should be written + - Default output path: parent of pathtobundle \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformance.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformance.java new file mode 100644 index 000000000..2c10bb036 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformance.java @@ -0,0 +1,232 @@ +package org.opencds.cqf.tooling.operations.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.TerserUtil; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ValidationOptions; +import ca.uhn.fhir.validation.ValidationResult; +import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.tooling.constants.Validation; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.NpmUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Operation(name = "ProfileConformance") +public class DataProfileConformance implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(DataProfileConformance.class); + + @OperationParam(alias = { "ptpd", "pathtopatientdata" }, setter = "setPathToPatientData", required = true, + description = "Path to the patient data represented as either a FHIR Bundle resource or as flat files within a directory (required).") + private String pathToPatientData; + @OperationParam(alias = { "purls", "packageurls" }, setter = "setPackageUrls", required = true, + description = "Urls for the FHIR packages to use for validation as a comma-separated list (required).") + private String packageUrls; + private List<String> packageUrlsList; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR OperationOutcome { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + description = "The directory path to which the FHIR OperationOutcome should be written (default is to replace existing resources within the IG)") + private String outputPath; + + @OperationParam(alias = {"numid", "numericidallowed"}, setter = "setNumericIdAllowed", defaultValue = "false", + description = "Determines if we want to allow numeric IDs (This overrides default HAPI behaviour") + private String numericIdAllowed; + + + private FhirContext fhirContext; + private FhirValidator validator; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + setGeneralValidator(); + IBaseBundle patientDataBundle; + if (IOUtils.isDirectory(pathToPatientData)) { + patientDataBundle = IOUtils.bundleResourcesInDirectory(pathToPatientData, fhirContext, true); + } else { + IBaseResource bundle = IOUtils.readResource(pathToPatientData, fhirContext); + if (bundle instanceof IBaseBundle) { + patientDataBundle = (IBaseBundle) bundle; + } else { + String invalidType = bundle.fhirType(); + logger.error("Expected a bundle resource at path {}, found {}", pathToPatientData, invalidType); + return; + } + } + + IOUtils.writeResources(validatePatientData(patientDataBundle), outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } + + public List<IBaseResource> validatePatientData(IBaseBundle patientData) { + List<IBaseResource> validatedResources = new ArrayList<>(); + + for (var patientDataResource : BundleUtil.toListOfResources(fhirContext, patientData)) { + ValidationOptions options = new ValidationOptions(); + String resourceType = patientDataResource.fhirType(); + if (profileMap.containsKey(resourceType)) { + profileMap.get(patientDataResource.fhirType()).forEach(options::addProfile); + } + ValidationResult result = validator.validateWithResult(patientDataResource, options); + if (!result.isSuccessful()) { + logger.warn("Validation errors found for {}/{} : {}", resourceType, + patientDataResource.getIdElement().getIdPart(), result.getMessages()); + tagResourceWithValidationResult(patientDataResource, result); + } else { + logger.info("Validation successful for {}/{}", resourceType, patientDataResource.getIdElement().getIdPart()); + } + validatedResources.add(patientDataResource); + } + + return validatedResources; + } + private void tagResourceWithValidationResult(IBaseResource resource, ValidationResult result) { + String id = UUID.randomUUID().toString(); + IDUtils.validateId(id, isNumericIdAllowed()); + + // create validation-result extension + ExtensionUtil.addExtension(fhirContext, resource, Validation.VALIDATION_RESULT_EXTENSION_URL, "Reference", "#" + id); + + // add outcome (validation messages) to the resource + IBaseOperationOutcome outcome = result.toOperationOutcome(); + outcome.setId(id); + TerserUtil.setField(fhirContext, "contained", resource, outcome); + } + + public void setGeneralValidator() { + NpmPackage npmPackage; + NpmUtils.PackageLoaderValidationSupport validationSupport = new NpmUtils.PackageLoaderValidationSupport(fhirContext); + for (String packageUrl : getPackageUrlsList()) { + try { + npmPackage = NpmPackage.fromUrl(packageUrl); + validationSupport.loadPackage(npmPackage); + } catch (IOException e) { + logger.warn("Encountered an issue when attempting to resolve package from URL: {}", packageUrl, e); + } + } + + populateProfileMap(validationSupport.fetchAllNonBaseStructureDefinitions()); + + ValidationSupportChain supportChain = new ValidationSupportChain(validationSupport, + new CommonCodeSystemsTerminologyService(fhirContext), + new DefaultProfileValidationSupport(fhirContext), + new InMemoryTerminologyServerValidationSupport(fhirContext), + new SnapshotGeneratingValidationSupport(fhirContext)); + + CachingValidationSupport cachingValidationSupport = new CachingValidationSupport(supportChain); + validator = fhirContext.newValidator(); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(cachingValidationSupport); + validator.registerValidatorModule(instanceValidator); + } + + private final Map<String, List<String>> profileMap = new HashMap<>(); + private void populateProfileMap(List<IBaseResource> structureDefinitions) { + if (structureDefinitions != null) { + FhirTerser terser = new FhirTerser(fhirContext); + for (var structureDefinition : structureDefinitions) { + String type = terser.getSinglePrimitiveValueOrNull(structureDefinition, "type"); + String url = terser.getSinglePrimitiveValueOrNull(structureDefinition, "url"); + if (type != null && url != null) { + profileMap.putIfAbsent(type, new ArrayList<>()); + if (!profileMap.get(type).contains(url)) { + profileMap.get(type).add(url); + } + } + } + } + } + + public String getPathToPatientData() { + return pathToPatientData; + } + + public void setPathToPatientData(String pathToPatientData) { + this.pathToPatientData = pathToPatientData; + } + + public void setPackageUrls(String packageUrls) { + this.packageUrls = packageUrls; + } + + public List<String> getPackageUrlsList() { + if (packageUrlsList == null && packageUrls != null) { + packageUrlsList = Arrays.stream(packageUrls.split(",")).map(String::trim).collect(Collectors.toList()); + } + return packageUrlsList; + } + + public void setPackageUrlsList(List<String> packageUrlsList) { + this.packageUrlsList = packageUrlsList; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public void setValidator(FhirValidator validator) { + this.validator = validator; + } + + public boolean isNumericIdAllowed() { + return Boolean.parseBoolean(numericIdAllowed); + } + + public void setNumericIdAllowed(String numericIdAllowed) { + this.numericIdAllowed = numericIdAllowed; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/README.md new file mode 100644 index 000000000..71ade1d21 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/validation/README.md @@ -0,0 +1,14 @@ +# ProfileConformance Operation + +The purpose of this operation is to determine whether the provided data conforms to a specified set of profiles. + +## Arguments: +- -pathtopatientdata | -ptpd (required) - Path to the patient data represented as either a FHIR Bundle resource or as +flat files within a directory. +- -packageurls | -purls (required) - Urls for the FHIR packages to use for validation as a comma-separated list (required). +- -version | -v (optional) - FHIR version { stu3, r4, r5 }. + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR resources { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path where the validated FHIR resources should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/validation/output \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/FhirTxExpansion.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/FhirTxExpansion.java new file mode 100644 index 000000000..d69b76c57 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/FhirTxExpansion.java @@ -0,0 +1,122 @@ +package org.opencds.cqf.tooling.operations.valueset.expansion; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Paths; + +@Operation(name = "FhirTxExpansion") +public class FhirTxExpansion implements ExecutableOperation { + private static final Logger logger = LoggerFactory.getLogger(FhirTxExpansion.class); + + @OperationParam(alias = { "pathtovalueset", "ptvs" }, setter = "setPathToValueSet", required = true, + description = "The path to the FHIR ValueSet resource(s) to be expanded (this may be a file or directory)") + private String pathToValueSet; + @OperationParam(alias = { "fhirserver", "fs" }, setter = "setFhirServer", defaultValue = "http://tx.fhir.org/r4", + description = "The FHIR server url that performs the $expand operation") + private String fhirServer; + // TODO: enable basic authorization with username and password params + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR ValueSet { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/terminology/output", + description = "The directory path to which the generated FHIR ValueSet resource should be written (default src/main/resources/org/opencds/cqf/tooling/terminology/output)") + private String outputPath; + + private FhirContext fhirContext; + private IGenericClient fhirServerClient; + + @Override + public void execute() { + fhirContext = FhirContextCache.getContext(version); + fhirServerClient = fhirContext.newRestfulGenericClient(fhirServer); + + if (Files.isDirectory(Paths.get(pathToValueSet))) { + for (IBaseResource resource : IOUtils.readResources(IOUtils.getFilePaths(pathToValueSet, true), fhirContext)) { + expandAndWriteValueSet(resource); + } + } else { + expandAndWriteValueSet(IOUtils.readResource(pathToValueSet, fhirContext)); + } + } + + public IBaseResource expandValueSet(IBaseResource valueSet) { + try { + IBaseParameters parameters = ParametersUtil.newInstance(fhirContext); + ParametersUtil.addParameterToParameters(fhirContext, parameters, "valueSet", valueSet); + return fhirServerClient.operation().onType("ValueSet") + .named("$expand").withParameters(parameters).execute(); + } catch (Exception e) { + logger.warn("Unable to expand: {}", valueSet.getIdElement().getValue(), e); + } + return null; + } + + private void expandAndWriteValueSet(IBaseResource resource) { + if (resource.fhirType().equalsIgnoreCase("valueset")) { + IBaseResource expandedVs = expandValueSet(resource); + if (expandedVs != null) { + IOUtils.writeResource(expandedVs, outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext); + } + } + } + + public String getPathToValueSet() { + return pathToValueSet; + } + + public void setPathToValueSet(String pathToValueSet) { + this.pathToValueSet = pathToValueSet; + } + + public String getFhirServer() { + return fhirServer; + } + + public void setFhirServer(String fhirServer) { + this.fhirServer = fhirServer; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/README.md new file mode 100644 index 000000000..ec8cb50f0 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/expansion/README.md @@ -0,0 +1,17 @@ +# FhirTxExpansion Operation + +The purpose of this operation is to run the $expand operation on a provided ValueSet resource to a provided FHIR +terminology service and return the expanded ValueSet resource. + +TODO - provide authentication support for the provided terminology service + +## Arguments: +- -pathtovalueset | -ptvs (required) - The path to the FHIR ValueSet resource(s) to be expanded (this may be a file or directory) +- -fhirserver | -fs - The FHIR server url that performs the $expand operation + - Default value: http://tx.fhir.org/r4 +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting expanded FHIR ValueSet resource { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the resulting expanded FHIR ValueSet resource should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/terminology/output \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/Config.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/Config.java new file mode 100644 index 000000000..dde8659c2 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/Config.java @@ -0,0 +1,580 @@ +package org.opencds.cqf.tooling.operations.valueset.generate.config; + +import ca.uhn.fhir.util.DateUtils; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Date; +import java.util.List; + +public class Config { + + @JsonProperty + String pathToIgResource; + + @JsonProperty + Author author; + + @JsonProperty("codesystems") + List<CodeSystems> codeSystems; + + @JsonAlias("valuesets") + @JsonProperty(required = true) + List<ValueSets> valueSets; + + public String getPathToIgResource() { + return pathToIgResource; + } + + public void setPathToIgResource(String pathToIgResource) { + this.pathToIgResource = pathToIgResource; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } + + public List<CodeSystems> getCodeSystems() { + return codeSystems; + } + + public void setCodeSystems(List<CodeSystems> codeSystems) { + this.codeSystems = codeSystems; + } + + public List<ValueSets> getValueSets() { + return valueSets; + } + + public void setValueSets(List<ValueSets> valueSets) { + this.valueSets = valueSets; + } + + static class Author { + @JsonProperty(required = true) + String name; + @JsonProperty(required = true) + String contactType; + @JsonProperty(required = true) + String contactValue; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContactType() { + return contactType; + } + + public void setContactType(String contactType) { + this.contactType = contactType; + } + + public String getContactValue() { + return contactValue; + } + + public void setContactValue(String contactValue) { + this.contactValue = contactValue; + } + } + + static class CodeSystems { + @JsonProperty(required = true) + String name; + @JsonProperty(required = true) + String url; + @JsonProperty(required = true) + String version; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + } + + static class ValueSets { + @JsonProperty(required = true) + String id; + @JsonProperty(required = true) + String canonical; + @JsonProperty + String version; + @JsonProperty + String name; + @JsonProperty + String title; + @JsonProperty + String status = "draft"; + @JsonProperty + Boolean experimental = true; + @JsonProperty + Date date = new Date(); + @JsonProperty + String publisher; + @JsonProperty + String description; + @JsonProperty + Jurisdiction jurisdiction; + @JsonProperty + String purpose; + @JsonProperty + String copyright; + @JsonProperty + List<String> profiles; + @JsonProperty + String clinicalFocus; + @JsonProperty + String dataElementScope; + @JsonProperty + String inclusionCriteria; + @JsonProperty + String exclusionCriteria; + @JsonProperty + String usageWarning; + @JsonProperty + List<String> knowledgeCapability; + @JsonProperty + List<String> knowledgeRepresentationLevel; + + @JsonProperty + RulesText rulesText; + + @JsonProperty + Hierarchy hierarchy; + + @JsonProperty + Expand expand; + + static class Jurisdiction { + @JsonProperty + Coding coding; + @JsonProperty + String text; + + public Coding getCoding() { + return coding; + } + + public void setCoding(Coding coding) { + this.coding = coding; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + static class Coding { + @JsonProperty + String code; + @JsonProperty + String system; + @JsonProperty + String display; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getSystem() { + return system; + } + + public void setSystem(String system) { + this.system = system; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + } + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCanonical() { + return canonical; + } + + public void setCanonical(String canonical) { + this.canonical = canonical; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Boolean getExperimental() { + return experimental; + } + + public void setExperimental(Boolean experimental) { + this.experimental = experimental; + } + + public Date getDate() { + return date; + } + + public void setDate(String date) { + this.date = DateUtils.parseDate(date); + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Jurisdiction getJurisdiction() { + return jurisdiction; + } + + public void setJurisdiction(Jurisdiction jurisdiction) { + this.jurisdiction = jurisdiction; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + public String getCopyright() { + return copyright; + } + + public void setCopyright(String copyright) { + this.copyright = copyright; + } + + public List<String> getProfiles() { + return profiles; + } + + public void setProfiles(List<String> profiles) { + this.profiles = profiles; + } + + public String getClinicalFocus() { + return clinicalFocus; + } + + public void setClinicalFocus(String clinicalFocus) { + this.clinicalFocus = clinicalFocus; + } + + public String getDataElementScope() { + return dataElementScope; + } + + public void setDataElementScope(String dataElementScope) { + this.dataElementScope = dataElementScope; + } + + public String getInclusionCriteria() { + return inclusionCriteria; + } + + public void setInclusionCriteria(String inclusionCriteria) { + this.inclusionCriteria = inclusionCriteria; + } + + public String getExclusionCriteria() { + return exclusionCriteria; + } + + public void setExclusionCriteria(String exclusionCriteria) { + this.exclusionCriteria = exclusionCriteria; + } + + public String getUsageWarning() { + return usageWarning; + } + + public void setUsageWarning(String usageWarning) { + this.usageWarning = usageWarning; + } + + public List<String> getKnowledgeCapability() { + return knowledgeCapability; + } + + public void setKnowledgeCapability(List<String> knowledgeCapability) { + this.knowledgeCapability = knowledgeCapability; + } + + public List<String> getKnowledgeRepresentationLevel() { + return knowledgeRepresentationLevel; + } + + public void setKnowledgeRepresentationLevel(List<String> knowledgeRepresentationLevel) { + this.knowledgeRepresentationLevel = knowledgeRepresentationLevel; + } + + public RulesText getRulesText() { + return rulesText; + } + + public void setRulesText(RulesText rulesText) { + this.rulesText = rulesText; + } + + public Hierarchy getHierarchy() { + return hierarchy; + } + + public void setHierarchy(Hierarchy hierarchy) { + this.hierarchy = hierarchy; + } + + public Expand getExpand() { + return expand; + } + + public void setExpand(Expand expand) { + this.expand = expand; + } + + static class RulesText { + @JsonProperty(required = true) + String narrative; + @JsonProperty(required = true) + String workflowXml; + @JsonProperty(required = true) + List<String> input; + @JsonProperty + List<String> includeFilter; + @JsonProperty + List<String> excludeFilter; + @JsonProperty + RulesText excludeRule; + + public String getNarrative() { + return narrative; + } + + public void setNarrative(String narrative) { + this.narrative = narrative; + } + + public String getWorkflowXml() { + return workflowXml; + } + + public void setWorkflowXml(String workflowXml) { + this.workflowXml = workflowXml; + } + + public List<String> getInput() { + return input; + } + + public void setInput(List<String> input) { + this.input = input; + } + + public List<String> getIncludeFilter() { + return includeFilter; + } + + public void setIncludeFilter(List<String> includeFilter) { + this.includeFilter = includeFilter; + } + + public List<String> getExcludeFilter() { + return excludeFilter; + } + + public void setExcludeFilter(List<String> excludeFilter) { + this.excludeFilter = excludeFilter; + } + + public RulesText getExcludeRule() { + return excludeRule; + } + + public void setExcludeRule(RulesText excludeRule) { + this.excludeRule = excludeRule; + } + } + + static class Hierarchy { + @JsonProperty(required = true) + String query; + @JsonProperty(required = true) + Auth auth; + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Auth getAuth() { + return auth; + } + + public void setAuth(Auth auth) { + this.auth = auth; + } + } + + static class Expand { + @JsonProperty(required = true) + String pathToValueSet; + @JsonProperty + TxServer txServer; + + public String getPathToValueSet() { + return pathToValueSet; + } + + public void setPathToValueSet(String pathToValueSet) { + this.pathToValueSet = pathToValueSet; + } + + public TxServer getTxServer() { + return txServer; + } + + public void setTxServer(TxServer txServer) { + this.txServer = txServer; + } + + static class TxServer { + @JsonProperty(required = true) + String baseUrl; + @JsonProperty + Auth auth; + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public Auth getAuth() { + return auth; + } + + public void setAuth(Auth auth) { + this.auth = auth; + } + } + } + + static class Auth { + @JsonProperty(required = true) + String user; + @JsonProperty(required = true) + String password; + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } + } + +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/ConfigValueSetGenerator.java b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/ConfigValueSetGenerator.java new file mode 100644 index 000000000..d01c02e94 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/ConfigValueSetGenerator.java @@ -0,0 +1,352 @@ +package org.opencds.cqf.tooling.operations.valueset.generate.config; + +import ca.uhn.fhir.context.FhirContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.CodeType; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.ContactDetail; +import org.hl7.fhir.r5.model.ContactPoint; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.ImplementationGuide; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.ValueSet; +import org.opencds.cqf.tooling.constants.Terminology; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.Operation; +import org.opencds.cqf.tooling.operations.OperationParam; +import org.opencds.cqf.tooling.operations.codesystem.loinc.HierarchyProcessor; +import org.opencds.cqf.tooling.operations.codesystem.rxnorm.RxMixWorkflowProcessor; +import org.opencds.cqf.tooling.operations.valueset.expansion.FhirTxExpansion; +import org.opencds.cqf.tooling.utilities.FhirContextCache; +import org.opencds.cqf.tooling.utilities.IDUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@Operation(name = "ValueSetsFromConfig") +public class ConfigValueSetGenerator implements ExecutableOperation { + private final Logger logger = LoggerFactory.getLogger(ConfigValueSetGenerator.class); + + @OperationParam(alias = { "pathtoconfig", "ptc" }, setter = "setPathToConfig", required = true, + description = "The path to the JSON configuration file.") + private String pathToConfig; + @OperationParam(alias = { "e", "encoding" }, setter = "setEncoding", defaultValue = "json", + description = "The file format to be used for representing the resulting FHIR ValueSets { json, xml } (default json)") + private String encoding; + @OperationParam(alias = { "v", "version" }, setter = "setVersion", defaultValue = "r4", + description = "FHIR version { stu3, r4, r5 } (default r4)") + private String version; + @OperationParam(alias = { "op", "outputpath" }, setter = "setOutputPath", + defaultValue = "src/main/resources/org/opencds/cqf/tooling/terminology/output", + description = "The directory path to which the generated FHIR ValueSet resources should be written (default src/main/resources/org/opencds/cqf/tooling/terminology/output)") + private String outputPath; + + @OperationParam(alias = {"numid", "numericidallowed"}, setter = "setNumericIdAllowed", defaultValue = "false", + description = "Determines if we want to allow numeric IDs (This overrides default HAPI behaviour") + private String numericIdAllowed; + + private FhirContext fhirContext; + + private final HierarchyProcessor hierarchyProcessor = new HierarchyProcessor(); + private final RxMixWorkflowProcessor rxMixWorkflowProcessor = new RxMixWorkflowProcessor(); + private final FhirTxExpansion fhirTxExpansion = new FhirTxExpansion(); + + @Override + public void execute() { + ObjectMapper mapper = new ObjectMapper(); + Config config; + try { + config = mapper.readValue(new File(pathToConfig), Config.class); + } catch (IOException e) { + String message = "Error reading configuration: " + e.getMessage(); + logger.error(message); + throw new IllegalArgumentException(message); + } + + fhirContext = FhirContextCache.getContext(version); + generateValueSets(config).forEach( + vs -> IOUtils.writeResource(vs, outputPath, IOUtils.Encoding.valueOf(encoding), fhirContext) + ); + } + + public List<IBaseResource> generateValueSets(Config config) { + CommonMetaData commonMetaData = null; + if (config.getPathToIgResource() != null) { + IBaseResource igResource = IOUtils.readResource(config.pathToIgResource, fhirContext); + ImplementationGuide ig = (ImplementationGuide) ResourceAndTypeConverter.convertToR5Resource(fhirContext, igResource); + commonMetaData = new CommonMetaData(ig, config.getAuthor()); + } + + List<IBaseResource> valueSets = new ArrayList<>(); + for (var valueSet : config.getValueSets()) { + ValueSet vs; + if (valueSet.getHierarchy() != null) { + prepareHierarchyProcessor(valueSet.getHierarchy()); + vs = (ValueSet) ResourceAndTypeConverter.convertToR5Resource(fhirContext, hierarchyProcessor.getValueSet()); + } else if (valueSet.getRulesText() != null) { + prepareRxMixWorkflowProcessor(valueSet.getRulesText()); + vs = (ValueSet) ResourceAndTypeConverter.convertToR5Resource(fhirContext, rxMixWorkflowProcessor.getValueSet()); + } else if (valueSet.getExpand() != null) { + prepareFhirTxExpansion(valueSet.getExpand()); + vs = (ValueSet) ResourceAndTypeConverter.convertToR5Resource(fhirContext, fhirTxExpansion.expandValueSet( + IOUtils.readResource(valueSet.getExpand().getPathToValueSet(), fhirContext))); + } else { + logger.warn("Unable to determine operation for {}, skipping...", valueSet.getId()); + continue; + } + valueSets.add(ResourceAndTypeConverter.convertFromR5Resource( + fhirContext, updateValueSet(vs, valueSet, commonMetaData))); + } + + return valueSets; + } + + private ValueSet updateValueSet(ValueSet vsToUpdate, Config.ValueSets configMetaData, CommonMetaData commonMetaData) { + ValueSet updatedValueSet = new ValueSet(); + + IDUtils.validateId(configMetaData.getId(), isNumericIdAllowed()); + + // metadata + updatedValueSet.setId(configMetaData.getId()); + updatedValueSet.setUrl(configMetaData.getCanonical()); + updatedValueSet.setVersion(configMetaData.getVersion() == null && commonMetaData != null ? commonMetaData.getVersion() : configMetaData.getVersion()); + updatedValueSet.setName(configMetaData.getName()); + updatedValueSet.setTitle(configMetaData.getTitle()); + updatedValueSet.setStatus(configMetaData.getStatus() == null ? null : Enumerations.PublicationStatus.fromCode(configMetaData.getStatus())); + updatedValueSet.setExperimental(configMetaData.getExperimental()); + updatedValueSet.setDate(configMetaData.getDate()); + updatedValueSet.setPublisher(configMetaData.getPublisher() == null && commonMetaData != null ? commonMetaData.getPublisher() : configMetaData.getPublisher()); + updatedValueSet.setDescription(configMetaData.getDescription()); + updatedValueSet.setJurisdiction(configMetaData.getJurisdiction() == null && commonMetaData != null ? commonMetaData.getJurisdiction() : resolveJurisdiction(configMetaData.getJurisdiction())); + updatedValueSet.setPurpose(configMetaData.getPurpose()); + updatedValueSet.setCopyright(configMetaData.getCopyright() == null && commonMetaData != null ? commonMetaData.getCopyright() : configMetaData.getCopyright()); + if (configMetaData.getProfiles() != null) { + configMetaData.getProfiles().forEach(profile -> updatedValueSet.getMeta().addProfile(profile)); + } + + // extensions + updatedValueSet.setExtension(vsToUpdate.getExtension()); + if (commonMetaData != null && commonMetaData.getAuthor() != null) { + updatedValueSet.addExtension(Terminology.VS_AUTHOR_EXT_URL, commonMetaData.getAuthor()); + } else if (commonMetaData != null && commonMetaData.getContact() != null) { + for (var contact : commonMetaData.getContact()) { + updatedValueSet.addExtension(Terminology.VS_AUTHOR_EXT_URL, contact); + } + } + if (configMetaData.getClinicalFocus() != null) { + updatedValueSet.addExtension(Terminology.CLINICAL_FOCUS_EXT_URL, new StringType(configMetaData.getClinicalFocus())); + } + if (configMetaData.getDataElementScope() != null) { + updatedValueSet.addExtension(Terminology.DATA_ELEMENT_SCOPE_EXT_URL, new StringType(configMetaData.getDataElementScope())); + } + if (configMetaData.getInclusionCriteria() != null) { + updatedValueSet.addExtension(Terminology.VS_INCLUSION_CRITERIA_EXT_URL, new StringType(configMetaData.getInclusionCriteria())); + } + if (configMetaData.getExclusionCriteria() != null) { + updatedValueSet.addExtension(Terminology.VS_EXCLUSION_CRITERIA_EXT_URL, new StringType(configMetaData.getExclusionCriteria())); + } + if (configMetaData.getUsageWarning() != null) { + updatedValueSet.addExtension(Terminology.USAGE_WARNING_EXT_URL, new StringType(configMetaData.getUsageWarning())); + } + if (configMetaData.getKnowledgeCapability() != null) { + for (String kc : configMetaData.getKnowledgeCapability()) { + updatedValueSet.addExtension(Terminology.KNOWLEDGE_CAPABILITY_EXT_URL, new CodeType(kc)); + } + } + if (configMetaData.getKnowledgeRepresentationLevel() != null) { + for (String krl : configMetaData.getKnowledgeRepresentationLevel()) { + updatedValueSet.addExtension(Terminology.KNOWLEDGE_REPRESENTATION_LEVEL_EXT_URL, new CodeType(krl)); + } + } + + // expansion + if (vsToUpdate.hasExpansion()) { + updatedValueSet.setExpansion(vsToUpdate.getExpansion()); + } else { + ValueSet.ValueSetExpansionComponent expansion = new ValueSet.ValueSetExpansionComponent(); + expansion.setTimestamp(new Date()); + List<Coding> codes = getCodesFromCompose(vsToUpdate); + expansion.setTotal(codes.size()); + for (var code : codes) { + expansion.addContains().setCode(code.getCode()).setSystem(code.getSystem()).setDisplay(code.getDisplay()).setVersion(code.getVersion()); + } + updatedValueSet.setExpansion(expansion); + } + + return updatedValueSet; + } + + // helper functions/classes + private List<Coding> getCodesFromCompose(ValueSet vs) { + List<Coding> codes = new ArrayList<>(); + for (var compose : vs.getCompose().getInclude()) { + for (var concept : compose.getConcept()) { + codes.add(new Coding().setSystem(compose.getSystem()).setCode(concept.getCode()).setDisplay(concept.getDisplay()).setVersion(compose.getVersion())); + } + } + return codes; + } + + private List<CodeableConcept> resolveJurisdiction(Config.ValueSets.Jurisdiction jurisdiction) { + if (jurisdiction == null) { + return null; + } + String code = jurisdiction.getCoding().getCode(); + String system = jurisdiction.getCoding().getSystem(); + String display = jurisdiction.getCoding().getDisplay(); + return Collections.singletonList(new CodeableConcept().addCoding(new Coding().setCode(code).setSystem(system).setDisplay(display))); + } + + private void prepareHierarchyProcessor(Config.ValueSets.Hierarchy hierarchy) { + hierarchyProcessor.setFhirContext(fhirContext); + hierarchyProcessor.setVersion(version); + hierarchyProcessor.setEncoding(encoding); + hierarchyProcessor.setQuery(hierarchy.getQuery()); + if (hierarchyProcessor.getUsername() == null) { + hierarchyProcessor.setUsername(hierarchy.getAuth().getUser()); + } + if (hierarchyProcessor.getPassword() == null) { + hierarchyProcessor.setPassword(hierarchy.getAuth().getPassword()); + } + } + + private void prepareRxMixWorkflowProcessor(Config.ValueSets.RulesText rulesText) { + rxMixWorkflowProcessor.setFhirContext(fhirContext); + rxMixWorkflowProcessor.setVersion(version); + rxMixWorkflowProcessor.setEncoding(encoding); + rxMixWorkflowProcessor.setRulesText(rulesText.getNarrative()); + rxMixWorkflowProcessor.setWorkflow(rulesText.getWorkflowXml()); + rxMixWorkflowProcessor.setInputs(rulesText.getInput()); + rxMixWorkflowProcessor.setIncludeFilters(rulesText.getIncludeFilter()); + rxMixWorkflowProcessor.setExcludeFilters(rulesText.getExcludeFilter()); + } + + private void prepareFhirTxExpansion(Config.ValueSets.Expand expand) { + fhirTxExpansion.setFhirContext(fhirContext); + fhirTxExpansion.setVersion(version); + fhirTxExpansion.setEncoding(encoding); + fhirTxExpansion.setFhirServer(expand.getTxServer().getBaseUrl()); + } + + public Logger getLogger() { + return logger; + } + + public String getPathToConfig() { + return pathToConfig; + } + + public void setPathToConfig(String pathToConfig) { + this.pathToConfig = pathToConfig; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getOutputPath() { + return outputPath; + } + + public void setOutputPath(String outputPath) { + this.outputPath = outputPath; + } + + public void setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + public boolean isNumericIdAllowed() { + return Boolean.parseBoolean(numericIdAllowed); + } + + public void setNumericIdAllowed(String numericIdAllowed) { + this.numericIdAllowed = numericIdAllowed; + } + + static class CommonMetaData { + private String version; + private String publisher; + private String copyright; + private ContactDetail author; + private List<ContactDetail> contact; + private List<CodeableConcept> jurisdiction; + + public CommonMetaData() { + version = null; + publisher = null; + copyright = null; + contact = null; + jurisdiction = null; + } + + public CommonMetaData(ImplementationGuide ig, Config.Author author) { + if (ig.hasVersion()) { + this.version = ig.getVersion(); + } + if (ig.hasPublisher()) { + this.publisher = ig.getPublisher(); + } + if (ig.hasCopyright()) { + this.copyright = ig.getCopyright(); + } + if (author != null) { + this.author = new ContactDetail().setName(author.getName()).setTelecom(Collections.singletonList( + new ContactPoint().setSystem(ContactPoint.ContactPointSystem.fromCode(author.getContactType())) + .setValue(author.contactValue))); + } + if (ig.hasContact()) { + this.contact = ig.getContact(); + } + if (ig.hasJurisdiction()) { + this.jurisdiction = ig.getJurisdiction(); + } + } + + public String getVersion() { + return version; + } + + public String getPublisher() { + return publisher; + } + + public String getCopyright() { + return copyright; + } + + public ContactDetail getAuthor() { + return author; + } + + public List<ContactDetail> getContact() { + return contact; + } + + public List<CodeableConcept> getJurisdiction() { + return jurisdiction; + } + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/README.md b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/README.md new file mode 100644 index 000000000..26be0f002 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operations/valueset/generate/config/README.md @@ -0,0 +1,114 @@ +# ValueSet Generation Tooling + +This operation is designed to generate FHIR ValueSet resources from a JSON configuration file. There is built-in +support for the RxNORM and LOINC code systems using existing APIs. Other code systems like CPT, SNOMEDCT, etc are +resolved using a specified FHIR server $expand operation. + +This operation uses the LoincHierarchy, RxMixWorkflow, and FhirTxExpansion operations in concert with the supplied +configuration file to generate the ValueSet resource. Additionally, the ResolveTerminologyFSN operation may be needed +to provide fully specified names for the codes included within the returned ValueSet resources. Note that the +ResolveTerminologyFSN is an expensive operation and should only be used when necessary. + +Note that this operation is a prototype. Any constructive feedback would be greatly appreciated. + +## Configuration + +```json +{ + "pathToIgResource": "path to the xml encoded FHIR ImplementationGuide resource within the IG", + "author": { + "name": "name of the author or organization responsible for creating the set of ValueSet resources", + "contactType": "email | phone | url | other", + "contactValue": "the contact details" + }, + "codesystems": [ + { + "name": "name of the code system", + "url": "canonical code system url", + "version": "the code system version - NOTE: these versions will be used for rulesText (RxNorm) and hierarchy (LOINC) value set generation, but no validation will be performed - use with caution. By default, the latest version will be used for the rulesText (RxNorm) and hierarchy (LOINC) code systems" + } + ], + "valuesets": [ + { + "id": "the value set unique id (string) - maps to ValueSet.id - required", + "canonical": "the value set canonical url - maps to ValueSet.url - required", + "version": "business version of the value set - maps to ValueSet.version", + "name": "the value set name (computer friendly) - maps to ValueSet.name", + "title": "the value set title (human friendly) - maps to ValueSet.title", + "status": "the status (depicting the lifecycle) of the value set: draft | active | retired | unknown (default is draft) - maps to ValueSet.status", + "experimental": true, + "date": "the date when the value set was created or revised (default value is today) - maps to ValueSet.date", + "publisher": "name of the organization or individual responsible for creating the value set - maps to ValueSet.publisher", + "description": "the value set description including why the value set was built, comments about misuse, instructions for clinical use and interpretation, literature references, examples from the paper world, etc - maps to ValueSet.description", + "jurisdiction": [ + { + "coding": { + "code": "legal or geographic region code in which this value set is intended to be used - maps to ValueSet.jurisdiction.coding.code", + "system": "legal or geographic region code system in which this value set is intended to be used - maps to ValueSet.jurisdiction.coding.system", + "display": "a representation of the meaning of the code in the system - maps to ValueSet.jurisdiction.coding.display" + }, + "text": "a human language representation of the egal or geographic region in which this value set is intended to be used - maps to ValueSet.jurisdiction.text" + } + ], + "purpose": "an explanation of why this value set is needed and why it has been designed as it has - maps to ValueSet.purpose", + "copyright": "copyright statement relating to the value set and/or its contents - maps to ValueSet.copyright", + "profiles": [ + "Canonical profile URL - maps to ValueSet.meta.profile" + ], + "clinicalFocus": "describes the clinical focus for the value set - maps to ValueSet.extension(http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-clinical-focus)", + "dataElementScope": "describes the data element scope (i.e. Condition, Medication, etc) for the value set - maps to ValueSet.extension(http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-dataelement-scope)", + "inclusionCriteria": "describes the inclusion criteria scope for the value set - maps to ValueSet.extension(http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-inclusion-criteria)", + "exclusionCriteria": "describes the exclusion criteria scope for the value set - maps to ValueSet.extension(http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/cdc-valueset-exclusion-criteria)", + "usageWarning": "an extra warning about the correct use of the value set - maps to ValueSet.extension(http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-usageWarning)", + "knowledgeCapability": [ + "defines a knowledge capability afforded by this value set: shareable | computable | publishable | executable - maps to ValueSet.extension(http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability)" + ], + "knowledgeRepresentationLevel": [ + "defines a knowledge representation level provided by this value set: narrative | semi-structured | structured | executable - maps to ValueSet.extension(http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel)" + ], + "rulesText": { + "narrative": "an expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g instructions that could only be followed by a human - maps to ValueSet.extension(http://hl7.org/fhir/StructureDefinition/valueset-rules-text)", + "workflowXml": "the RxMix workflow library content", + "input": [ + "the input values needed to run the workflow" + ], + "includeFilter": [ + "the string values to include in the result - values that don't match will be excluded" + ], + "excludeFilter": [ + "the string values to exclude from the result - values that don't match will be included" + ], + "excludeRule": { + // same as rulesText + } + }, + "hierarchy": { + "query": "an expression that provides an alternative definition of the content of the value set in some form that is not computable - e.g instructions that could only be followed by a human - not currently mapped to ValueSet", + "auth": { + "user": "username for LOINC account", + "password": "password for LOINC account" + } + }, + "expand": { + "pathToValueSet": "path to the ValueSet resource file", + "txServer": { + "baseUrl": "base url for the fhir terminology server", + "auth": { + "user": "username for fhir the terminology server", + "password": "password for the fhir terminology server" + } + } + } + } + ] +} +``` + +## Arguments: +- -pathtoconfig | -ptc (required) - The path to the JSON configuration file. +- -version | -v (optional) - FHIR version { stu3, r4, r5 } + - Default version: r4 +- -encoding | -e (optional) - The file format to be used for representing the resulting FHIR ValueSet resources { json, xml }. + - Default encoding: json +- -outputpath | -op (optional) - The directory path to which the resulting FHIR ValueSet resources should be written. + - Default output path: src/main/resources/org/opencds/cqf/tooling/terminology/output \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGProcessor.java index 00e22dd06..cf6140813 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGProcessor.java @@ -129,7 +129,7 @@ public void refreshIG(RefreshIGParameters params) { Boolean versioned = params.versioned; // String fhirUri = params.fhirUri; String measureToRefreshPath = params.measureToRefreshPath; - ArrayList<String> resourceDirs = params.resourceDirs; + List<String> resourceDirs = params.resourceDirs; if (resourceDirs.size() == 0) { try { resourceDirs = IGUtils.extractResourcePaths(this.rootDir, this.sourceIg); @@ -203,7 +203,7 @@ public static String getBundlesPath(String igPath) { public static final String testCasePathElement = "input/tests/"; public static final String devicePathElement = "input/resources/device/"; - public static void ensure(String igPath, Boolean includePatientScenarios, Boolean includeTerminology, ArrayList<String> resourcePaths) { + public static void ensure(String igPath, Boolean includePatientScenarios, Boolean includeTerminology, List<String> resourcePaths) { File directory = new File(getBundlesPath(igPath)); if (!directory.exists()) { directory.mkdir(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGTestProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGTestProcessor.java index b1fe08bfe..8738331b1 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGTestProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGTestProcessor.java @@ -206,7 +206,7 @@ public void testIg(TestIGParameters params) { ITestProcessor testProcessor = getResourceTypeTestProcessor(group.getName()); List<Map.Entry<String, IBaseResource>> testCasesBundles = - BundleUtils.GetBundlesInDir(testArtifact.getPath(), fhirContext, false); + BundleUtils.getBundlesInDir(testArtifact.getPath(), fhirContext, false); for (Map.Entry<String, IBaseResource> testCaseBundleMapEntry : testCasesBundles) { IBaseResource testCaseBundle = testCaseBundleMapEntry.getValue(); @@ -284,7 +284,7 @@ private Map.Entry<String, IBaseResource> getContentBundleForTestArtifact(String String contentBundlePath = getPathForContentBundleTestArtifact(groupName, testArtifactName); File testArtifactContentBundleDirectory = new File(contentBundlePath); if (testArtifactContentBundleDirectory != null && testArtifactContentBundleDirectory.exists()) { - List<Map.Entry<String, IBaseResource>> testArtifactContentBundles = BundleUtils.GetBundlesInDir(contentBundlePath, fhirContext, false); + List<Map.Entry<String, IBaseResource>> testArtifactContentBundles = BundleUtils.getBundlesInDir(contentBundlePath, fhirContext, false); // NOTE: Making the assumption that there will be a single bundle for the artifact. testArtifactContentBundle = testArtifactContentBundles.get(0); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PostBundlesInDirProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PostBundlesInDirProcessor.java index bf30e39f7..264b2860c 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PostBundlesInDirProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PostBundlesInDirProcessor.java @@ -56,7 +56,7 @@ public static void PostBundlesInDir(PostBundlesInDirParameters params) { Encoding encoding = params.encoding; FhirContext fhirContext = getFhirContext(fhirVersion); - List<Map.Entry<String, IBaseResource>> resources = BundleUtils.GetBundlesInDir(params.directoryPath, fhirContext); + List<Map.Entry<String, IBaseResource>> resources = BundleUtils.getBundlesInDir(params.directoryPath, fhirContext); resources.forEach(entry -> postBundleToFhirUri(fhirUri, encoding, fhirContext, entry.getValue())); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/ScaffoldProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/ScaffoldProcessor.java index 5b6ab8148..d4d4723eb 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/ScaffoldProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/ScaffoldProcessor.java @@ -49,21 +49,11 @@ public void scaffold(ScaffoldParameters params) { } private void EnsureLibraryPath() { - try { - IOUtils.ensurePath(igPath + LibraryPath); - } - catch (IOException ex) { - LogUtils.putException("EnsureLibraryPath", ex.getMessage()); - } + IOUtils.ensurePath(igPath + LibraryPath); } private void EnsureMeasurePath() { - try { - IOUtils.ensurePath(igPath + MeasurePath); - } - catch (IOException ex) { - LogUtils.putException("EnsureMeasurePath", ex.getMessage()); - } + IOUtils.ensurePath(igPath + MeasurePath); } public void createLibrary(String name) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/BundleUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/BundleUtils.java index 8fa5f2519..ce73188ac 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/BundleUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/BundleUtils.java @@ -10,6 +10,7 @@ import java.util.UUID; import java.util.stream.Collectors; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Resource; @@ -20,6 +21,17 @@ public class BundleUtils { + private BundleUtils() {} + + public static BundleTypeEnum getBundleType(String bundleTypeName) { + for (BundleTypeEnum bundleTypeEnum : BundleTypeEnum.values()) { + if (bundleTypeEnum.getCode().equalsIgnoreCase(bundleTypeName)) { + return bundleTypeEnum; + } + } + return null; + } + @SafeVarargs public static Object bundleArtifacts(String id, List<IBaseResource> resources, FhirContext fhirContext, Boolean addBundleTimestamp, List<Object>... identifiers) { for (IBaseResource resource : resources) { @@ -67,7 +79,7 @@ public static org.hl7.fhir.r4.model.Bundle bundleR4Artifacts(String id, List<IBa org.hl7.fhir.r4.model.Bundle bundle = new org.hl7.fhir.r4.model.Bundle(); ResourceUtils.setIgId(id, bundle, false); bundle.setType(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION); - if (addBundleTimestamp) { + if (Boolean.TRUE.equals(addBundleTimestamp)) { bundle.setTimestamp((new Date())); } if (identifiers!= null && !identifiers.isEmpty()) { @@ -105,11 +117,11 @@ public static void postBundle(IOUtils.Encoding encoding, FhirContext fhirContext } } - public static List<Map.Entry<String, IBaseResource>> GetBundlesInDir(String directoryPath, FhirContext fhirContext) { - return GetBundlesInDir(directoryPath, fhirContext, true); + public static List<Map.Entry<String, IBaseResource>> getBundlesInDir(String directoryPath, FhirContext fhirContext) { + return getBundlesInDir(directoryPath, fhirContext, true); } - public static List<Map.Entry<String, IBaseResource>> GetBundlesInDir(String directoryPath, FhirContext fhirContext, Boolean recursive) { + public static List<Map.Entry<String, IBaseResource>> getBundlesInDir(String directoryPath, FhirContext fhirContext, Boolean recursive) { File dir = new File(directoryPath); if (!dir.isDirectory()) { throw new IllegalArgumentException("path to directory must be an existing directory."); @@ -142,7 +154,7 @@ public static List<Map.Entry<String, IBaseResource>> GetBundlesInDir(String dire public static void stampDstu3BundleEntriesWithSoftwareSystems(org.hl7.fhir.dstu3.model.Bundle bundle, List<CqfmSoftwareSystem> softwareSystems, FhirContext fhirContext, String rootDir) { for (org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent entry: bundle.getEntry()) { org.hl7.fhir.dstu3.model.Resource resource = entry.getResource(); - if ((resource.fhirType().equals("Library")) || ((resource.fhirType().equals("Measure")))) { + if ((resource.fhirType().equals("Library")) || (resource.fhirType().equals("Measure"))) { org.opencds.cqf.tooling.common.stu3.CqfmSoftwareSystemHelper cqfmSoftwareSystemHelper = new org.opencds.cqf.tooling.common.stu3.CqfmSoftwareSystemHelper(rootDir); cqfmSoftwareSystemHelper.ensureSoftwareSystemExtensionAndDevice((org.hl7.fhir.dstu3.model.DomainResource)resource, softwareSystems, fhirContext); } @@ -185,9 +197,8 @@ public static void extractR4Resources(org.hl7.fhir.r4.model.Bundle bundle, Strin } } - public static ArrayList<Resource> getR4ResourcesFromBundle(Bundle bundle){ + public static List<Resource> getR4ResourcesFromBundle(Bundle bundle){ ArrayList <Resource> resourceArrayList = new ArrayList<>(); - FhirContext context = FhirContext.forR4Cached(); for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent entry : bundle.getEntry()) { org.hl7.fhir.r4.model.Resource entryResource = entry.getResource(); if (entryResource != null) { @@ -197,7 +208,7 @@ public static ArrayList<Resource> getR4ResourcesFromBundle(Bundle bundle){ return resourceArrayList; } - public static ArrayList<org.hl7.fhir.dstu3.model.Resource> getStu3ResourcesFromBundle(org.hl7.fhir.dstu3.model.Bundle bundle){ + public static List<org.hl7.fhir.dstu3.model.Resource> getStu3ResourcesFromBundle(org.hl7.fhir.dstu3.model.Bundle bundle){ ArrayList <org.hl7.fhir.dstu3.model.Resource> resourceArrayList = new ArrayList<>(); for (org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent entry : bundle.getEntry()) { org.hl7.fhir.dstu3.model.Resource entryResource = entry.getResource(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/CanonicalUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/CanonicalUtils.java index c74c94dcb..1e2fbc7fc 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/CanonicalUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/CanonicalUtils.java @@ -2,10 +2,16 @@ import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.fhir.r4.model.CanonicalType; +import org.opencds.cqf.tooling.exception.InvalidCanonical; public class CanonicalUtils { + private CanonicalUtils() {} + public static String getHead(String url) { + if (url == null) { + return null; + } int index = url.lastIndexOf("/"); if (index == -1) { return null; @@ -17,6 +23,9 @@ public static String getHead(String url) { } public static String getTail(String url) { + if (url == null) { + return null; + } int index = url.lastIndexOf("/"); if (index == -1) { return null; @@ -50,7 +59,7 @@ public static String getId(CanonicalType canonical) { return getId(canonical.getValue()); } - throw new RuntimeException("CanonicalType must have a value for id extraction"); + throw new InvalidCanonical("CanonicalType must have a value for id extraction"); } public static String getResourceName(CanonicalType canonical) { @@ -63,7 +72,7 @@ public static String getResourceName(CanonicalType canonical) { return null; } - throw new RuntimeException("CanonicalType must have a value for resource name extraction"); + throw new InvalidCanonical("CanonicalType must have a value for resource name extraction"); } public static VersionedIdentifier toVersionedIdentifier(String url) { @@ -75,7 +84,7 @@ public static VersionedIdentifier toVersionedIdentifier(String url) { String head = getHead(url); String resourceName = getTail(head); if (resourceName == null || !resourceName.equals("Library")) { - throw new RuntimeException("Cannot extract versioned identifier from a non-library canonical"); + throw new InvalidCanonical("Cannot extract versioned identifier from a non-library canonical"); } String base = getHead(head); if ("".equals(base)) { @@ -84,4 +93,22 @@ public static VersionedIdentifier toVersionedIdentifier(String url) { return new VersionedIdentifier().withSystem(base).withId(id).withVersion(version); } + + public static VersionedIdentifier toVersionedIdentifierAnyResource(String url) { + String version = getVersion(url); + if ("".equals(version)) { + version = null; + } + String id = getId(url); + String head = getHead(url); + String base = null; + if (head != null) { + base = getHead(head); + } + if ("".equals(base)) { + base = null; + } + + return new VersionedIdentifier().withSystem(base).withId(id).withVersion(version); + } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirContextCache.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirContextCache.java index a13f8c111..e9d6ccbec 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirContextCache.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirContextCache.java @@ -1,6 +1,6 @@ package org.opencds.cqf.tooling.utilities; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Map; import java.util.Objects; @@ -9,7 +9,9 @@ public class FhirContextCache { - private final static Map<FhirVersionEnum, FhirContext> contextCache = new HashMap<>(); + private static final Map<FhirVersionEnum, FhirContext> contextCache = new EnumMap<>(FhirVersionEnum.class); + + private FhirContextCache() {} /** * @param fhirVersion The FHIR version to get a context for (e.g. "DSTU3", "4.0", etc.) @@ -34,11 +36,7 @@ public static FhirContext getContext(String fhirVersion) { */ public static synchronized FhirContext getContext(FhirVersionEnum fhirVersion) { Objects.requireNonNull(fhirVersion, "fhirVersion can not be null"); - - if (!contextCache.containsKey(fhirVersion)) { - contextCache.put(fhirVersion, fhirVersion.newContext()); - } - + contextCache.computeIfAbsent(fhirVersion, k -> fhirVersion.newContext()); return contextCache.get(fhirVersion); } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirVersionUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirVersionUtils.java index 510646d71..9ce2246d4 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirVersionUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/FhirVersionUtils.java @@ -5,6 +5,9 @@ import ca.uhn.fhir.context.FhirVersionEnum; public class FhirVersionUtils { + + private FhirVersionUtils() {} + /** * This method handles numeric versions (3, 2.0, 4.0.1, etc.) and release versions (R4, DSTU3, etc.) * Partial versions are allowed. The minimum compatible version supported by the tooling returned diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index c0c24cc52..56048095b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -4,18 +4,25 @@ import java.io.IOException; import java.io.InputStreamReader; +import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; import ca.uhn.fhir.context.FhirContext; public class HttpClientUtils { + + private HttpClientUtils() {} + public static void post(String fhirServerUrl, IBaseResource resource, Encoding encoding, FhirContext fhirContext) throws IOException { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { @@ -25,14 +32,8 @@ public static void post(String fhirServerUrl, IBaseResource resource, Encoding e String resourceString = IOUtils.encodeResourceAsString(resource, encoding, fhirContext); StringEntity input = new StringEntity(resourceString); post.setEntity(input); - HttpResponse response = httpClient.execute(post); - BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); - String responseMessage = ""; - String line = ""; - while ((line = rd.readLine()) != null) { - responseMessage += line; - } - if (responseMessage.indexOf("error") > -1) { + String response = getResponse(httpClient.execute(post)); + if (response.contains("error")) { throw new IOException("Error posting resource to FHIR server (" + fhirServerUrl + "). Resource was not posted : " + resource.getIdElement().getIdPart()); } } @@ -41,16 +42,30 @@ public static void post(String fhirServerUrl, IBaseResource resource, Encoding e public static String get(String path) throws IOException { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { HttpGet get = new HttpGet(path); + return getResponse(httpClient.execute(get)); + } + } - HttpResponse response = httpClient.execute(get); - BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); - String responseMessage = ""; - String line = ""; - while ((line = rd.readLine()) != null) { - responseMessage += line; - } - - return responseMessage; + public static String getResponse(HttpResponse response) throws IOException { + BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + StringBuilder responseMessage = new StringBuilder(); + String line; + while ((line = rd.readLine()) != null) { + responseMessage.append(line); } + + return responseMessage.toString(); + } + + public static ResponseHandler<String> getDefaultResponseHandler() { + return response -> { + int status = response.getStatusLine().getStatusCode(); + if (status >= 200 && status < 300) { + HttpEntity entity = response.getEntity(); + return entity != null ? EntityUtils.toString(entity) : null; + } else { + throw new ClientProtocolException("Unexpected response status: " + status); + } + }; } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IDUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IDUtils.java new file mode 100644 index 000000000..b25651d7c --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IDUtils.java @@ -0,0 +1,111 @@ +package org.opencds.cqf.tooling.utilities; + +import org.opencds.cqf.tooling.exception.InvalidIdException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.regex.Pattern; + +public class IDUtils { + + private static final Logger logger = LoggerFactory.getLogger(IDUtils.class); + + // regex defined https://www.hl7.org/fhir/datatypes.html#id + private static final String fhirRegex = "[A-Za-z0-9\\-\\.]{1,64}"; + + private static Pattern idPattern; + + private static Pattern getIdPattern() { + if(idPattern == null) { + idPattern = Pattern.compile(fhirRegex); + } + return idPattern; + } + + // validateId determines which validation strategy to use for the provided id based on the boolean parameter. + public static void validateId(String id, boolean allowNumericIds) { + if (allowNumericIds) { + validateIdPattern(id); + } else { + validateIdAlphanumeric(id); + } + } + + // validateIdAlphanumeric checks that the provided id matches the fhir defined regex pattern & contains letters to + // satisfy requirements for HAPI server. An InvalidIdException is thrown if these conditions are not met. + private static void validateIdAlphanumeric(String id){ + if (!getIdPattern().matcher(id).find() || !id.matches(".*[a-zA-z]+.*") || id.length() > 64) { + logger.error("Provided id: {} is not an alphanumeric string matching regex: {}", id, fhirRegex); + throw new InvalidIdException("The provided id is not an alphanumeric string matching regex pattern."); + } + } + + // validateIdPattern checks that the provided id matches the fhir defined regex pattern. This allows ids to be numeric + // which overrides default HAPI Server behaviour. An InvalidIdException is thrown if these conditions are not met. + private static void validateIdPattern(String id){ + if (!getIdPattern().matcher(id).find() || id.length() > 64) { + logger.error("Provided id: {} does not match regex: {}", id, fhirRegex); + throw new InvalidIdException("The provided id does not match regex pattern."); + } + } + + public static String toId(String name, boolean allowNumericId) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + + if (name.endsWith(".")) { + name = name.substring(0, name.lastIndexOf(".")); + } + + name = name.toLowerCase().trim() + // remove these characters + .replace("(", "").replace(")", "").replace("[", "").replace("]", "").replace("\n", "") + // replace these with ndash + .replace(":", "-") + .replace(",", "-") + .replace("_", "-") + .replace("/", "-") + .replace(" ", "-") + .replace(".", "-") + // remove multiple ndash + .replace("----", "-").replace("---", "-").replace("--", "-").replace(">", "greater-than") + .replace("<", "less-than"); + + validateId(name, allowNumericId); + return name; + } + + public static String toUpperId(String name, boolean allowNumericId) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + + if (name.endsWith(".")) { + name = name.substring(0, name.lastIndexOf(".")); + } + + name = name.trim() + // remove these characters + .replace("(", "").replace(")", "").replace("[", "").replace("]", "").replace("\n", "") + .replace(":", "") + .replace(",", "") + .replace("_", "") + .replace("/", "") + .replace(" ", "") + .replace(".", "") + .replace("-", "") + .replace(">", "") + .replace("<", ""); + + validateId(name, allowNumericId); + return name; + } + + public static String libraryNameToId(String name, String version, boolean allowNumericId) { + String nameAndVersion = "library-" + name + "-" + version; + nameAndVersion = nameAndVersion.replace("_", "-"); + validateId(nameAndVersion, allowNumericId); + return nameAndVersion; + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IGUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IGUtils.java index 630c81523..10daa2977 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IGUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IGUtils.java @@ -2,13 +2,28 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import ca.uhn.fhir.context.FhirContext; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.utilities.Utilities; +import org.opencds.cqf.tooling.exception.IGInitializationException; +import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IGUtils { + private static final Logger logger = LoggerFactory.getLogger(IGUtils.class); private IGUtils () {} public static String getImplementationGuideCanonicalBase(String url) { String canonicalBase = null; @@ -20,8 +35,8 @@ public static String getImplementationGuideCanonicalBase(String url) { return canonicalBase; } - public static ArrayList<String> extractResourcePaths(String rootDir, ImplementationGuide sourceIg) throws IOException { - ArrayList<String> result = new ArrayList<>(); + public static List<String> extractResourcePaths(String rootDir, ImplementationGuide sourceIg) throws IOException { + List<String> result = new ArrayList<>(); for (ImplementationGuide.ImplementationGuideDefinitionParameterComponent p : sourceIg.getDefinition().getParameter()) { if (p.getCode().equals("path-resource")) { result.add(Utilities.path(rootDir, p.getValue())); @@ -87,4 +102,331 @@ protected static File tryDirectory(String rootDir, String path) { return new File(combinedPath); } + + public static class IGInfo { + private final FhirContext fhirContext; + private final String rootDir; + private final String iniPath; + private final String igPath; + private final String cqlBinaryPath; + private final String resourcePath; + private final String libraryResourcePath; + private boolean refreshLibraries = true; + private final String planDefinitionResourcePath; + private boolean refreshPlanDefinitions = true; + private final String measureResourcePath; + private boolean refreshMeasures = true; + private final String valueSetResourcePath; + private final String codeSystemResourcePath; + private final String activityDefinitionResourcePath; + private final String questionnaireResourcePath; + private final ImplementationGuide igResource; + private final String packageId; + private final String canonical; + + public IGInfo(FhirContext fhirContext, String rootDir) { + if (fhirContext == null) { + this.fhirContext = FhirContext.forR4Cached(); + logger.info("The FHIR context was not provided, using {}", + this.fhirContext.getVersion().getVersion().getFhirVersionString()); + } + else { + this.fhirContext = fhirContext; + } + if (rootDir == null) { + throw new IGInitializationException("The root directory path for the IG not provided"); + } + this.rootDir = rootDir; + this.iniPath = getIniPath(); + this.igPath = getIgPath(); + this.cqlBinaryPath = getCqlBinaryPath(); + this.resourcePath = getResourcePath(); + this.libraryResourcePath = getLibraryResourcePath(); + this.planDefinitionResourcePath = getPlanDefinitionResourcePath(); + this.measureResourcePath = getMeasureResourcePath(); + this.valueSetResourcePath = getValueSetResourcePath(); + this.codeSystemResourcePath = getCodeSystemResourcePath(); + this.activityDefinitionResourcePath = getActivityDefinitionResourcePath(); + this.questionnaireResourcePath = getQuestionnaireResourcePath(); + this.igResource = getIgResource(); + this.packageId = getPackageId(); + this.canonical = getCanonical(); + } + + public FhirContext getFhirContext() { + return fhirContext; + } + + public String getRootDir() { + return rootDir; + } + + public String getIniPath() { + if (this.iniPath != null) { + return this.iniPath; + } + try (Stream<Path> walk = Files.walk(Paths.get(this.rootDir))) { + List<String> pathList = walk.filter(p -> !Files.isDirectory(p)) + .map(p -> p.toString().toLowerCase()) + .filter(f -> f.endsWith("ig.ini")) + .collect(Collectors.toList()); + if (pathList.isEmpty()) { + logger.error("Unable to determine path to IG ini file"); + throw new IGInitializationException("An IG ini file must be present! See https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#igroot for more information."); + } + else if (pathList.size() > 1) { + logger.warn("Found multiple IG ini files, using {}", pathList.get(0)); + } + return pathList.get(0); + } catch (IOException ioe) { + logger.error("Error determining path to IG ini file"); + throw new IGInitializationException(ioe.getMessage(), ioe); + } + } + + public String getIgPath() { + if (this.igPath != null) { + return this.igPath; + } + try { + List<String> igList = FileUtils.readLines(new File(iniPath), StandardCharsets.UTF_8) + .stream().filter(s -> s.startsWith("ig")).map( + s -> StringUtils.deleteWhitespace(s).replace("ig=", "")) + .collect(Collectors.toList()); + if (igList.isEmpty()) { + logger.error("Unable to determine path to IG resource file"); + throw new IGInitializationException("An IG resource file must be present! See https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#igroot-input for more information."); + } + else if (igList.size() > 1) { + logger.warn("Found multiple IG resource files, using {}", igList.get(0)); + } + return FilenameUtils.concat(rootDir, igList.get(0)); + } catch (IOException ioe) { + logger.error("Error determining path to IG resource file"); + throw new IGInitializationException(ioe.getMessage(), ioe); + } + } + + public String getCqlBinaryPath() { + if (this.cqlBinaryPath != null) { + return this.cqlBinaryPath; + } + // preferred directory structure + String candidate = FilenameUtils.concat(getRootDir(), "input/cql"); + if (new File(candidate).isDirectory()) { + return candidate; + } + // support legacy directory structure + candidate = FilenameUtils.concat(getRootDir(), "input/pagecontent/cql"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + String message = "Unable to locate CQL binary directory, Please see https://github.com/cqframework/sample-content-ig#directory-structure for guidance on content IG directory structure."; + logger.error(message); + throw new IGInitializationException(message); + } + } + + public String getResourcePath() { + if (this.resourcePath != null) { + return this.resourcePath; + } + String candidate = FilenameUtils.concat(getRootDir(), "input/resources"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + String message = "Unable to locate the resources directory, Please see https://github.com/cqframework/sample-content-ig#directory-structure for guidance on content IG directory structure."; + logger.error(message); + throw new IGInitializationException(message); + } + } + + public String getLibraryResourcePath() { + if (this.libraryResourcePath != null) { + return this.libraryResourcePath; + } + if (refreshLibraries) { + String candidate = FilenameUtils.concat(getResourcePath(), "library"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the Library resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + return null; + } + + public boolean isRefreshLibraries() { + return this.refreshLibraries; + } + + public void setRefreshLibraries(boolean refreshLibraries) { + this.refreshLibraries = refreshLibraries; + } + + public String getPlanDefinitionResourcePath() { + if (this.planDefinitionResourcePath != null) { + return this.planDefinitionResourcePath; + } + if (refreshPlanDefinitions) { + String candidate = FilenameUtils.concat(getResourcePath(), "plandefinition"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the PlanDefinition resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + return null; + } + + public boolean isRefreshPlanDefinitions() { + return this.refreshPlanDefinitions; + } + + public void setRefreshPlanDefinitions(boolean refreshPlanDefinitions) { + this.refreshPlanDefinitions = refreshPlanDefinitions; + } + + public String getMeasureResourcePath() { + if (this.measureResourcePath != null) { + return this.measureResourcePath; + } + if (refreshPlanDefinitions) { + String candidate = FilenameUtils.concat(getResourcePath(), "measure"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the Measure resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + return null; + } + + public boolean isRefreshMeasures() { + return this.refreshMeasures; + } + + public void setRefreshMeasures(boolean refreshMeasures) { + this.refreshMeasures = refreshMeasures; + } + + public String getValueSetResourcePath() { + if (this.valueSetResourcePath != null) { + return this.valueSetResourcePath; + } + String candidate = FilenameUtils.concat(getResourcePath(), "vocabulary/valueset"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getResourcePath(), "vocabulary"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getRootDir(), "input/vocabulary/valueset"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getRootDir(), "input/vocabulary"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the ValueSet resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + + public String getCodeSystemResourcePath() { + if (this.codeSystemResourcePath != null) { + return this.codeSystemResourcePath; + } + String candidate = FilenameUtils.concat(getResourcePath(), "vocabulary/codesystem"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getResourcePath(), "codesystem"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getRootDir(), "input/vocabulary/codesystem"); + if (new File(candidate).isDirectory()) { + return candidate; + } + candidate = FilenameUtils.concat(getRootDir(), "input/codesystem"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the CodeSystem resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + + public String getActivityDefinitionResourcePath() { + if (this.activityDefinitionResourcePath != null) { + return this.activityDefinitionResourcePath; + } + String candidate = FilenameUtils.concat(getResourcePath(), "activitydefinition"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the ActivityDefinition resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + + public String getQuestionnaireResourcePath() { + if (this.questionnaireResourcePath != null) { + return this.questionnaireResourcePath; + } + String candidate = FilenameUtils.concat(getResourcePath(), "questionnaire"); + if (new File(candidate).isDirectory()) { + return candidate; + } else { + logger.warn("Unable to locate the Questionnaire resource directory. The base resources path will be used."); + return getResourcePath(); + } + } + + public ImplementationGuide getIgResource() { + if (this.igResource != null) { + return this.igResource; + } + switch (this.fhirContext.getVersion().getVersion()) { + case DSTU3: + return (ImplementationGuide) ResourceAndTypeConverter.stu3ToR5Resource(IOUtils.readResource(igPath, this.fhirContext)); + case R4: + return (ImplementationGuide) ResourceAndTypeConverter.r4ToR5Resource(IOUtils.readResource(igPath, this.fhirContext)); + case R5: return (ImplementationGuide) IOUtils.readResource(igPath, this.fhirContext); + default: throw new IGInitializationException( + "Unsupported FHIR context: " + this.fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public String getPackageId() { + if (this.packageId != null) { + return this.packageId; + } + if (!getIgResource().hasPackageId()) { + String message = "A package ID must be present in the IG resource"; + logger.error(message); + throw new IGInitializationException(message); + } + return getIgResource().getPackageId(); + } + + public String getCanonical() { + if (this.canonical != null) { + return this.canonical; + } + if (!getIgResource().hasUrl()) { + String message = "A canonical must be present in the IG resource"; + logger.error(message); + throw new IGInitializationException(message); + } + String url = getIgResource().getUrl(); + return url.contains("/ImplementationGuide/") ? url.substring(0, url.indexOf("/ImplementationGuide/")) : url; + } + } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java index e74696214..074bb2cff 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java @@ -17,14 +17,19 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import ca.uhn.fhir.util.BundleBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.cqframework.cql.cql2elm.CqlCompilerException; @@ -33,6 +38,7 @@ import org.cqframework.cql.cql2elm.LibraryManager; import org.cqframework.cql.cql2elm.ModelManager; import org.cqframework.cql.elm.tracking.TrackBack; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.Utilities; import org.opencds.cqf.tooling.library.LibraryProcessor; @@ -41,60 +47,60 @@ import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.parser.IParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class IOUtils -{ - - public enum Encoding - { - CQL("cql"), JSON("json"), XML("xml"), UNKNOWN(""); - - private String string; - - public String toString() - { - return this.string; - } - - private Encoding(String string) - { - this.string = string; +public class IOUtils { + private static final Logger logger = LoggerFactory.getLogger(IOUtils.class); + + public enum Encoding { + CQL("cql"), JSON("json"), XML("xml"), UNKNOWN(""); + + private final String value; + + @Override + public String toString() { + return this.value; + } + + private Encoding(String string) { + this.value = string; } public static Encoding parse(String value) { if (value == null) { return UNKNOWN; } - + switch (value.trim().toLowerCase()) { case "cql": return CQL; - case "json": + case "json": return JSON; case "xml": return XML; - default: + default: return UNKNOWN; } } - } + } - public static ArrayList<String> resourceDirectories = new ArrayList<String>(); + public static List<String> resourceDirectories = new ArrayList<>(); public static String getIdFromFileName(String fileName) { - return fileName.replaceAll("_", "-"); + return fileName.replace("_", "-"); } public static byte[] encodeResource(IBaseResource resource, Encoding encoding, FhirContext fhirContext) { return encodeResource(resource, encoding, fhirContext, false); } - public static byte[] encodeResource(IBaseResource resource, Encoding encoding, FhirContext fhirContext, boolean prettyPrintOutput) - { + public static byte[] encodeResource(IBaseResource resource, Encoding encoding, FhirContext fhirContext, + boolean prettyPrintOutput) { if (encoding == Encoding.UNKNOWN) { return new byte[] { }; } - IParser parser = getParser(encoding, fhirContext); + IParser parser = getParser(encoding, fhirContext); return parser.setPrettyPrint(prettyPrintOutput).encodeResourceToString(resource).getBytes(); } @@ -102,87 +108,87 @@ public static String getFileContent(File file) { try { return FileUtils.readFileToString(file, StandardCharsets.UTF_8); } catch (IOException e) { - e.printStackTrace(); + logger.error(e.getMessage()); throw new RuntimeException("Error reading file: " + e.getMessage()); } } - public static String encodeResourceAsString(IBaseResource resource, Encoding encoding, FhirContext fhirContext) - { + public static String encodeResourceAsString(IBaseResource resource, Encoding encoding, FhirContext fhirContext) { if (encoding == Encoding.UNKNOWN) { return ""; } - IParser parser = getParser(encoding, fhirContext); - return parser.setPrettyPrint(true).encodeResourceToString(resource).toString(); + IParser parser = getParser(encoding, fhirContext); + return parser.setPrettyPrint(true).encodeResourceToString(resource); } // Issue 96 - adding second signature to allow for passing versioned - public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, FhirContext fhirContext) - { - writeResource(resource, path, encoding, fhirContext, true); + public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, + FhirContext fhirContext) { + writeResource(resource, path, encoding, fhirContext, true); } - - public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, FhirContext fhirContext, Boolean versioned) { - writeResource(resource, path, encoding, fhirContext, true, null, true); + + public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, + FhirContext fhirContext, Boolean versioned) { + writeResource(resource, path, encoding, fhirContext, versioned, null, true); } - public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, FhirContext fhirContext, Boolean versioned, String outputFileName) { - writeResource(resource, path, encoding, fhirContext, true, outputFileName, true); + public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, + FhirContext fhirContext, Boolean versioned, + String outputFileName) { + writeResource(resource, path, encoding, fhirContext, versioned, outputFileName, true); } - public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, FhirContext fhirContext, Boolean versioned, boolean prettyPrintOutput) { - writeResource(resource, path, encoding, fhirContext, true, null, prettyPrintOutput); + public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, + FhirContext fhirContext, Boolean versioned, + boolean prettyPrintOutput) { + writeResource(resource, path, encoding, fhirContext, versioned, null, prettyPrintOutput); } - public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, FhirContext fhirContext, Boolean versioned, String outputFileName, boolean prettyPrintOutput) { + public static <T extends IBaseResource> void writeResource(T resource, String path, Encoding encoding, + FhirContext fhirContext, Boolean versioned, + String outputFileName, boolean prettyPrintOutput) { // If the path is to a specific resource file, just re-use that file path/name. - String outputPath = null; + String outputPath; File file = new File(path); if (file.isFile()) { outputPath = path; } else { - try { - ensurePath(path); - } - catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Error writing Resource to file: " + e.getMessage()); - } + ensurePath(path); - String baseName = null; + String baseName; if (outputFileName == null || outputFileName.isBlank()) { baseName = resource.getIdElement().getIdPart(); } else { baseName = outputFileName; } - // Issue 96 - // If includeVersion is false then just use name and not id for the file baseName - if (!versioned) { - // Assumes that the id will be a string with - separating the version number - // baseName = baseName.split("-")[0]; - } + // Issue 96 + // If includeVersion is false then just use name and not id for the file baseName + if (Boolean.FALSE.equals(versioned)) { + // Assumes that the id will be a string with - separating the version number + // baseName = baseName.split("-")[0]; + } outputPath = FilenameUtils.concat(path, formatFileName(baseName, encoding, fhirContext)); } - try (FileOutputStream writer = new FileOutputStream(outputPath)) - { + try (FileOutputStream writer = new FileOutputStream(outputPath)) { writer.write(encodeResource(resource, encoding, fhirContext, prettyPrintOutput)); writer.flush(); - } - catch (IOException e) - { - e.printStackTrace(); + } catch (IOException e) { + logger.error(e.getMessage()); throw new RuntimeException("Error writing Resource to file: " + e.getMessage()); } } - public static <T extends IBaseResource> void writeResources(Map<String, T> resources, String path, Encoding encoding, FhirContext fhirContext) - { - for (Map.Entry<String, T> set : resources.entrySet()) - { + public static <T extends IBaseResource> void writeResources(List<T> resources, String path, + Encoding encoding, FhirContext fhirContext) { + resources.forEach(resource -> writeResource(resource, path, encoding, fhirContext)); + } + + public static <T extends IBaseResource> void writeResources(Map<String, T> resources, String path, Encoding encoding, FhirContext fhirContext) { + for (Map.Entry<String, T> set : resources.entrySet()) { writeResource(set.getValue(), path, encoding, fhirContext); } } @@ -218,9 +224,8 @@ public static void copyFile(String inputPath, String outputPath) { Path src = Paths.get(inputPath); Path dest = Paths.get(outputPath); Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING); - } - catch (IOException e) { - e.printStackTrace(); + } catch (IOException e) { + logger.error(e.getMessage()); throw new RuntimeException("Error copying file: " + e.getMessage()); } } @@ -230,7 +235,6 @@ public static String getTypeQualifiedResourceId(String path, FhirContext fhirCon if (resource != null) { return resource.getIdElement().getResourceType() + "/" + resource.getIdElement().getIdPart(); } - return null; } @@ -256,23 +260,21 @@ public static String getCanonicalResourceVersion(IBaseResource resource, FhirCon public static IBaseResource readResource(String path, FhirContext fhirContext) { return readResource(path, fhirContext, false); } - + //users should always check for null - private static Map<String, IBaseResource> cachedResources = new LinkedHashMap<String, IBaseResource>(); - public static IBaseResource readResource(String path, FhirContext fhirContext, Boolean safeRead) - { + private static final Map<String, IBaseResource> cachedResources = new LinkedHashMap<>(); + public static IBaseResource readResource(String path, FhirContext fhirContext, Boolean safeRead) { Encoding encoding = getEncoding(path); if (encoding == Encoding.UNKNOWN || encoding == Encoding.CQL) { return null; } - IBaseResource resource = cachedResources.get(path); + IBaseResource resource = cachedResources.get(path); if (resource != null) { return resource; - } + } - try - { + try { IParser parser = getParser(encoding, fhirContext); File file = new File(path); @@ -280,40 +282,26 @@ public static IBaseResource readResource(String path, FhirContext fhirContext, B throw new IllegalArgumentException(String.format("Cannot read a resource from a directory: %s", path)); } - // if (!file.exists()) { - // String[] paths = file.getParent().split("\\\\"); - // file = new File(Paths.get(file.getParent(), paths[paths.length - 1] + "-" + file.getName()).toString()); - // } - - if (safeRead) { - if (!file.exists()) { - return null; - } + if (Boolean.TRUE.equals(safeRead) && !file.exists()) { + return null; } - try (FileReader reader = new FileReader(file)){ + try (FileReader reader = new FileReader(file)) { resource = parser.parseResource(reader); } cachedResources.put(path, resource); - } - catch (Exception e) - { + } catch (IOException e) { throw new RuntimeException(String.format("Error reading resource from path %s: %s", path, e.getMessage()), e); } return resource; } - public static void updateCachedResource(IBaseResource updatedResource, String path){ - if(null != cachedResources.get(path)){ - cachedResources.put(path, updatedResource); - } - + public static void updateCachedResource(IBaseResource updatedResource, String path) { + cachedResources.computeIfPresent(path, (key, value) -> updatedResource); } - public static List<IBaseResource> readResources(List<String> paths, FhirContext fhirContext) - { + public static List<IBaseResource> readResources(List<String> paths, FhirContext fhirContext) { List<IBaseResource> resources = new ArrayList<>(); - for (String path : paths) - { + for (String path : paths) { IBaseResource resource = readResource(path, fhirContext); if (resource != null) { resources.add(resource); @@ -322,36 +310,75 @@ public static List<IBaseResource> readResources(List<String> paths, FhirContext return resources; } - public static List<String> getFilePaths(String directoryPath, Boolean recursive) - { - List<String> filePaths = new ArrayList<String>(); + public static IBaseResource readJsonResourceIgnoreElements(String path, FhirContext fhirContext, String... elements) { + Encoding encoding = getEncoding(path); + if (encoding == Encoding.UNKNOWN || encoding == Encoding.CQL || encoding == Encoding.XML) { + return null; + } + + if (cachedResources.containsKey(path)) { + return cachedResources.get(path); + } + + IParser parser = getParser(encoding, fhirContext); + try (FileReader reader = new FileReader(path)) { + JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject(); + Arrays.stream(elements).forEach(obj::remove); + IBaseResource resource = parser.parseResource(obj.toString()); + cachedResources.put(path, resource); + return resource; + } catch (IOException e) { + logger.error(e.getMessage()); + throw new RuntimeException(String.format("Error reading resource from path %s: %s", path, e)); + } + } + + public static IBaseBundle bundleResourcesInDirectory(String directoryPath, FhirContext fhirContext, Boolean recursive) { + BundleBuilder builder = new BundleBuilder(fhirContext); + Iterator<File> fileIterator = FileUtils.iterateFiles(new File(directoryPath), new String[]{ "xml", "json" }, recursive); + while (fileIterator.hasNext()) { + builder.addCollectionEntry(readResource(fileIterator.next().getAbsolutePath(), fhirContext)); + } + return builder.getBundle(); + } + + public static boolean isDirectory(String path) { + return FileUtils.isDirectory(new File(path)); + } + + public static List<String> getFilePaths(String directoryPath, Boolean recursive) { + List<String> filePaths = new ArrayList<>(); File inputDir = new File(directoryPath); - ArrayList<File> files = inputDir.isDirectory() ? new ArrayList<File>(Arrays.asList(Optional.ofNullable(inputDir.listFiles()).<NoSuchElementException>orElseThrow(() -> new NoSuchElementException()))) : new ArrayList<File>(); - + ArrayList<File> files = inputDir.isDirectory() + ? new ArrayList<>(Arrays.asList(Optional.ofNullable( + inputDir.listFiles()).orElseThrow(NoSuchElementException::new))) + : new ArrayList<>(); + for (File file : files) { if (file.isDirectory()) { - //note: this is not the same as anding recursive to isDirectory as that would result in directories being added to the list if the request is not recursive. - if (recursive) { + //note: this is not the same as ANDing recursive to isDirectory as that would result in directories + // being added to the list if the request is not recursive. + if (Boolean.TRUE.equals(recursive)) { filePaths.addAll(getFilePaths(file.getPath(), recursive)); } - } - else { - filePaths.add(file.getPath()); + } else { + filePaths.add(file.getPath()); } } return filePaths; } - public static String getResourceFileName(String resourcePath, IBaseResource resource, Encoding encoding, FhirContext fhirContext, boolean versioned, boolean prefixed) { + public static String getResourceFileName(String resourcePath, IBaseResource resource, Encoding encoding, + FhirContext fhirContext, boolean versioned, boolean prefixed) { String resourceVersion = IOUtils.getCanonicalResourceVersion(resource, fhirContext); String filename = resource.getIdElement().getIdPart(); // Issue 96 // Handle no version on filename but still in id if (!versioned && resourceVersion != null) { - int index = filename.indexOf(resourceVersion); - if (index > 0) { - filename = filename.substring(0, index - 1); - } + int index = filename.indexOf(resourceVersion); + if (index > 0) { + filename = filename.substring(0, index - 1); + } } else if (versioned && resourceVersion != null) { int index = filename.indexOf(resourceVersion); if (index < 0) { @@ -360,11 +387,8 @@ public static String getResourceFileName(String resourcePath, IBaseResource reso } String resourceType = resource.fhirType().toLowerCase(); - // Cannot read from here it isn't always set - //String resourceType = resource.getIdElement().getResourceType().toLowerCase(); - - String result = Paths.get(resourcePath, resourceType, (prefixed ? (resourceType + "-") : "") + filename) + getFileExtension(encoding); - return result; + return Paths.get(resourcePath, resourceType, (prefixed ? (resourceType + "-") : "") + + filename) + getFileExtension(encoding); } // Returns the parent directory if it is named resources, otherwise, the parent of that @@ -373,7 +397,6 @@ public static String getResourceDirectory(String path) { if (!result.toLowerCase().endsWith("resources")) { result = getParentDirectoryPath(result); } - return result; } @@ -382,22 +405,22 @@ public static String getParentDirectoryPath(String path) { return file.getParent(); } - public static List<String> getDirectoryPaths(String path, Boolean recursive) - { - List<String> directoryPaths = new ArrayList<String>(); - List<File> directories = new ArrayList<File>(); + public static List<String> getDirectoryPaths(String path, Boolean recursive) { + List<String> directoryPaths = new ArrayList<>(); + List<File> directories; File parentDirectory = new File(path); try { - directories = Arrays.asList(Optional.ofNullable(parentDirectory.listFiles()).<NoSuchElementException>orElseThrow(() -> new NoSuchElementException())); + directories = Arrays.asList(Optional.ofNullable(parentDirectory.listFiles()) + .orElseThrow(NoSuchElementException::new)); } catch (Exception e) { - System.out.println("No paths found for the Directory " + path + ":"); + logger.error("No paths found for the Directory {}:", path); return directoryPaths; } - - + + for (File directory : directories) { if (directory.isDirectory()) { - if (recursive) { + if (Boolean.TRUE.equals(recursive)) { directoryPaths.addAll(getDirectoryPaths(directory.getPath(), recursive)); } directoryPaths.add(directory.getPath()); @@ -412,21 +435,24 @@ public static void initializeDirectory(String path) { try { deleteDirectory(path); } catch (IOException e) { - e.printStackTrace(); + logger.error(e.getMessage()); throw new RuntimeException("Error deleting directory: " + path + " - " + e.getMessage()); } } - directory.mkdir(); + + if (!directory.mkdir()) { + logger.warn("Unable to initialize directory at {}", path); + } } public static void deleteDirectory(String path) throws IOException { - Files.walkFileTree(Paths.get(path), new SimpleFileVisitor<Path>() { + Files.walkFileTree(Paths.get(path), new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { Files.delete(file); // this will work because it's always a File return FileVisitResult.CONTINUE; } - + @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); //this will work because Files in the directory are already deleted @@ -441,99 +467,90 @@ public static Encoding getEncoding(String path) } //users should protect against Encoding.UNKNOWN or Enconding.CQL - private static IParser getParser(Encoding encoding, FhirContext fhirContext) - { + private static IParser getParser(Encoding encoding, FhirContext fhirContext) { switch (encoding) { - case XML: + case XML: return fhirContext.newXmlParser(); case JSON: return fhirContext.newJsonParser(); - default: - throw new RuntimeException("Unknown encoding type: " + encoding.toString()); + default: + throw new RuntimeException("Unknown encoding type: " + encoding); } } - public static Boolean pathEndsWithElement(String igPath, String pathElement) - { - Boolean result = false; - try - { + public static Boolean pathEndsWithElement(String igPath, String pathElement) { + boolean result = false; + try { String baseElement = FilenameUtils.getBaseName(igPath).equals("") ? FilenameUtils.getBaseName(FilenameUtils.getFullPathNoEndSeparator(igPath)) : FilenameUtils.getBaseName(igPath); result = baseElement.equals(pathElement); - } - catch (Exception e) {} + } catch (Exception ignored) {} return result; } - public static List<String> getDependencyCqlPaths(String cqlContentPath, Boolean includeVersion) throws Exception { - ArrayList<File> DependencyFiles = getDependencyCqlFiles(cqlContentPath, includeVersion); - ArrayList<String> DependencyPaths = new ArrayList<String>(); - for (File file : DependencyFiles) { - DependencyPaths.add(file.getPath().toString()); + public static List<String> getDependencyCqlPaths(String cqlContentPath, Boolean includeVersion) { + List<File> dependencyFiles = getDependencyCqlFiles(cqlContentPath, includeVersion); + List<String> dependencyPaths = new ArrayList<>(); + for (File file : dependencyFiles) { + dependencyPaths.add(file.getPath()); } - return DependencyPaths; + return dependencyPaths; } - public static ArrayList<File> getDependencyCqlFiles(String cqlContentPath, Boolean includeVersion) throws Exception { + public static List<File> getDependencyCqlFiles(String cqlContentPath, Boolean includeVersion) { File cqlContent = new File(cqlContentPath); File cqlContentDir = cqlContent.getParentFile(); if (!cqlContentDir.isDirectory()) { throw new IllegalArgumentException("The specified path to library files is not a directory"); } - ArrayList<String> dependencyLibraries = ResourceUtils.getIncludedLibraryNames(cqlContentPath, includeVersion); + + List<String> dependencyLibraries = ResourceUtils.getIncludedLibraryNames(cqlContentPath, includeVersion); File[] allCqlContentFiles = cqlContentDir.listFiles(); - if (allCqlContentFiles.length == 1) { - return new ArrayList<File>(); - } ArrayList<File> dependencyCqlFiles = new ArrayList<>(); - for (File cqlFile : allCqlContentFiles) { - if (dependencyLibraries.contains(getIdFromFileName(cqlFile.getName().replace(".cql", "")))) { - dependencyCqlFiles.add(cqlFile); - dependencyLibraries.remove(getIdFromFileName(cqlFile.getName().replace(".cql", ""))); - } + + if (allCqlContentFiles != null) { + if (allCqlContentFiles.length == 1) { + return new ArrayList<>(); + } + for (File cqlFile : allCqlContentFiles) { + if (dependencyLibraries.contains(getIdFromFileName(cqlFile.getName().replace(".cql", "")))) { + dependencyCqlFiles.add(cqlFile); + dependencyLibraries.remove(getIdFromFileName(cqlFile.getName().replace(".cql", ""))); + } + } } - if (dependencyLibraries.size() != 0) { - String message = (dependencyLibraries.size()) + " included cql Libraries not found: "; - + if (!dependencyLibraries.isEmpty()) { + StringBuilder message = new StringBuilder().append(dependencyLibraries.size()) + .append(" included cql Libraries not found: "); + for (String includedLibrary : dependencyLibraries) { - message += "\r\n" + includedLibrary + " MISSING"; - } - throw new Exception(message); - } + message.append("\r\n").append(includedLibrary).append(" MISSING"); + } + throw new RuntimeException(message.toString()); + } return dependencyCqlFiles; - } - - private static Map<String, CqlTranslator> cachedTranslator = new LinkedHashMap<String, CqlTranslator>(); + } + + private static final Map<String, CqlTranslator> cachedTranslator = new LinkedHashMap<>(); public static CqlTranslator translate(String cqlContentPath, ModelManager modelManager, LibraryManager libraryManager, CqlTranslatorOptions options) { CqlTranslator translator = cachedTranslator.get(cqlContentPath); if (translator != null) { return translator; } try { - File cqlFile = new File(cqlContentPath); - if(!cqlFile.getName().endsWith(".cql")) { - throw new IllegalArgumentException("cqlContentPath must be a path to a .cql file"); - } - - // ArrayList<CqlTranslatorOptions.Options> options = new ArrayList<>(); - // options.add(CqlTranslatorOptions.Options.EnableDateRangeOptimization); - - translator = - CqlTranslator.fromFile( - cqlFile, - modelManager, - libraryManager, - null, options); - - if (translator.getErrors().size() > 0) { - //System.err.println("Translation failed due to errors:"); + File cqlFile = new File(cqlContentPath); + if (!cqlFile.getName().endsWith(".cql")) { + throw new IllegalArgumentException("cqlContentPath must be a path to a .cql file"); + } + + translator = CqlTranslator.fromFile(cqlFile, modelManager, libraryManager, null, options); + + if (!translator.getErrors().isEmpty()) { ArrayList<String> errors = new ArrayList<>(); for (CqlCompilerException error : translator.getErrors()) { TrackBack tb = error.getLocator(); String lines = tb == null ? "[n/a]" : String.format("[%d:%d, %d:%d]", tb.getStartLine(), tb.getStartChar(), tb.getEndLine(), tb.getEndChar()); - //System.err.printf("%s %s%n", lines, error.getMessage()); errors.add(lines + error.getMessage()); } throw new IllegalArgumentException(errors.toString()); @@ -541,9 +558,7 @@ public static CqlTranslator translate(String cqlContentPath, ModelManager modelM cachedTranslator.put(cqlContentPath, translator); return translator; } catch (IOException e) { - //e.printStackTrace(); - //throw new IllegalArgumentException("Error encountered during CQL translation: " + e.getMessage()); - throw new IllegalArgumentException("Error encountered during CQL translation"); + throw new IllegalArgumentException("Error encountered during CQL translation", e); } } @@ -556,7 +571,7 @@ public static String getCqlString(String cqlContentPath) { cql.append(line).append("\n"); } } catch (IOException e) { - e.printStackTrace(); + logger.error(e.getMessage()); throw new IllegalArgumentException("Error reading CQL file: " + cqlFile.getName()); } return cql.toString(); @@ -579,30 +594,26 @@ public static String formatFileName(String baseName, Encoding encoding, FhirCont default: igVersionToken = ""; } - String result = baseName + getFileExtension(encoding); + String result = baseName + getFileExtension(encoding); if (encoding == Encoding.CQL) { result = result.replace("-" + igVersionToken, "_" + igVersionToken); } return result; - } + } - public static List<String> putAllInListIfAbsent(List<String> values, List<String> list) - { + public static void putAllInListIfAbsent(List<String> values, List<String> list) { for (String value : values) { if (!list.contains(value)) { list.add(value); } } - return list; } - public static List<String> putInListIfAbsent(String value, List<String> list) - { + public static void putInListIfAbsent(String value, List<String> list) { if (!list.contains(value)) { list.add(value); } - return list; } public static String getLibraryPathAssociatedWithCqlFileName(String cqlPath, FhirContext fhirContext) { @@ -613,10 +624,10 @@ public static String getLibraryPathAssociatedWithCqlFileName(String cqlPath, Fhi // NOTE: A bit of a hack, but we need to support both xml and json encodings for existing resources and the long-term strategy is // to revisit this and change the approach to use the references rather than file name matching, so this should be good for the near-term. if (path.endsWith(libraryFileName.replaceAll(".cql", ".json")) - || path.endsWith(libraryFileName.replaceAll(".cql", ".xml")) - || path.endsWith(fileName.replaceAll(".cql", ".json")) - || path.endsWith(fileName.replaceAll(".cql", ".xml"))) - { + || path.endsWith(libraryFileName.replaceAll(".cql", ".xml")) + || path.endsWith(fileName.replaceAll(".cql", ".json")) + || path.endsWith(fileName.replaceAll(".cql", ".xml"))) + { libraryPath = path; break; } @@ -625,18 +636,18 @@ public static String getLibraryPathAssociatedWithCqlFileName(String cqlPath, Fhi return libraryPath; } - private static HashSet<String> cqlLibraryPaths = new LinkedHashSet<String>(); - public static HashSet<String> getCqlLibraryPaths() { + private static final HashSet<String> cqlLibraryPaths = new LinkedHashSet<>(); + public static Set<String> getCqlLibraryPaths() { if (cqlLibraryPaths.isEmpty()) { setupCqlLibraryPaths(); } return cqlLibraryPaths; } - private static void setupCqlLibraryPaths() { - //need to add a error report for bad resource paths - for(String dir : resourceDirectories) { + private static void setupCqlLibraryPaths() { + //need to add an error report for bad resource paths + for (String dir : resourceDirectories) { List<String> filePaths = IOUtils.getFilePaths(dir, true); - filePaths.stream().filter(path -> path.contains(".cql")).forEach(path -> cqlLibraryPaths.add(path)); + filePaths.stream().filter(path -> path.contains(".cql")).forEach(cqlLibraryPaths::add); } } @@ -658,32 +669,30 @@ public static String getCqlLibrarySourcePath(String libraryName, String cqlFileN } } } - } - catch (IOException e) { - e.printStackTrace(); + } catch (IOException e) { + logger.error(e.getMessage()); LogUtils.putException(libraryName, e); } return cqlLibrarySourcePath; } - private static HashSet<String> terminologyPaths = new LinkedHashSet<String>(); - public static HashSet<String> getTerminologyPaths(FhirContext fhirContext) { + private static final HashSet<String> terminologyPaths = new LinkedHashSet<>(); + public static Set<String> getTerminologyPaths(FhirContext fhirContext) { if (terminologyPaths.isEmpty()) { setupTerminologyPaths(fhirContext); } return terminologyPaths; } private static void setupTerminologyPaths(FhirContext fhirContext) { - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for (String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { if (path.toLowerCase().contains("valuesets") || path.toLowerCase().contains("valueset")) { - System.out.println("Error reading in Terminology from path: " + path + "\n" + e); + logger.error("Error reading in Terminology from path: {} \n {}", path, e); } } } @@ -695,13 +704,13 @@ private static void setupTerminologyPaths(FhirContext fhirContext) { String conceptClassName = conceptDefinition.getImplementingClass().getName(); String codingClassName = codingDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> - valuesetClassName.equals(entry.getValue().getClass().getName()) - || conceptClassName.equals(entry.getValue().getClass().getName()) - || codingClassName.equals(entry.getValue().getClass().getName()) - ) - .forEach(entry -> terminologyPaths.add(entry.getKey())); + .filter(entry -> entry.getValue() != null) + .filter(entry -> + valuesetClassName.equals(entry.getValue().getClass().getName()) + || conceptClassName.equals(entry.getValue().getClass().getName()) + || codingClassName.equals(entry.getValue().getClass().getName()) + ) + .forEach(entry -> terminologyPaths.add(entry.getKey())); } } @@ -713,14 +722,14 @@ public static IBaseResource getLibraryByUrl(FhirContext fhirContext, String url) return library; } - private static HashSet<String> libraryPaths = new LinkedHashSet<String>(); - public static HashSet<String> getLibraryPaths(FhirContext fhirContext) { + private static final HashSet<String> libraryPaths = new LinkedHashSet<>(); + public static Set<String> getLibraryPaths(FhirContext fhirContext) { if (libraryPaths.isEmpty()) { setupLibraryPaths(fhirContext); } return libraryPaths; } - private static Map<String, IBaseResource> libraryUrlMap = new LinkedHashMap<String, IBaseResource>(); + private static final Map<String, IBaseResource> libraryUrlMap = new LinkedHashMap<>(); public static Map<String, IBaseResource> getLibraryUrlMap(FhirContext fhirContext) { if (libraryPathMap.isEmpty()) { setupLibraryPaths(fhirContext); @@ -731,14 +740,14 @@ public static Map<String, IBaseResource> getLibraryUrlMap(FhirContext fhirContex } return libraryUrlMap; } - private static Map<String, String> libraryPathMap = new LinkedHashMap<String, String>(); + private static final Map<String, String> libraryPathMap = new LinkedHashMap<>(); public static Map<String, String> getLibraryPathMap(FhirContext fhirContext) { if (libraryPathMap.isEmpty()) { setupLibraryPaths(fhirContext); } return libraryPathMap; } - private static Map<String, IBaseResource> libraries = new LinkedHashMap<String, IBaseResource>(); + private static final Map<String, IBaseResource> libraries = new LinkedHashMap<>(); public static Map<String, IBaseResource> getLibraries(FhirContext fhirContext) { if (libraries.isEmpty()) { setupLibraryPaths(fhirContext); @@ -746,16 +755,15 @@ public static Map<String, IBaseResource> getLibraries(FhirContext fhirContext) { return libraries; } private static void setupLibraryPaths(FhirContext fhirContext) { - Map<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + Map<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { IBaseResource resource = IOUtils.readResource(path, fhirContext, true); resources.put(path, resource); } catch (Exception e) { if(path.toLowerCase().contains("library")) { - System.out.println("Error reading in Library from path: " + path + "\n" + e); + logger.error("Error reading in Library from path: {} \n {}", path, e); } } } @@ -764,32 +772,32 @@ private static void setupLibraryPaths(FhirContext fhirContext) { String libraryClassName = libraryDefinition.getImplementingClass().getName(); // BaseRuntimeChildDefinition urlElement = libraryDefinition.getChildByNameOrThrowDataFormatException("url"); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> libraryClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> { - libraryPaths.add(entry.getKey()); - libraries.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); - libraryPathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); - libraryUrlMap.put(ResourceUtils.getUrl(entry.getValue(), fhirContext), entry.getValue()); - }); + .filter(entry -> entry.getValue() != null) + .filter(entry -> libraryClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> { + libraryPaths.add(entry.getKey()); + libraries.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); + libraryPathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); + libraryUrlMap.put(ResourceUtils.getUrl(entry.getValue(), fhirContext), entry.getValue()); + }); } } - private static HashSet<String> measurePaths = new LinkedHashSet<String>(); - public static HashSet<String> getMeasurePaths(FhirContext fhirContext) { + private static final HashSet<String> measurePaths = new LinkedHashSet<>(); + public static Set<String> getMeasurePaths(FhirContext fhirContext) { if (measurePaths.isEmpty()) { setupMeasurePaths(fhirContext); } return measurePaths; } - private static Map<String, String> measurePathMap = new LinkedHashMap<String, String>(); + private static final Map<String, String> measurePathMap = new LinkedHashMap<>(); public static Map<String, String> getMeasurePathMap(FhirContext fhirContext) { if (measurePathMap.isEmpty()) { setupMeasurePaths(fhirContext); } return measurePathMap; } - private static Map<String, IBaseResource> measures = new LinkedHashMap<String, IBaseResource>(); + private static final Map<String, IBaseResource> measures = new LinkedHashMap<>(); public static Map<String, IBaseResource> getMeasures(FhirContext fhirContext) { if (measures.isEmpty()) { setupMeasurePaths(fhirContext); @@ -797,16 +805,15 @@ public static Map<String, IBaseResource> getMeasures(FhirContext fhirContext) { return measures; } private static void setupMeasurePaths(FhirContext fhirContext) { - Map<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + Map<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { IBaseResource resource = IOUtils.readResource(path, fhirContext, true); resources.put(path, resource); } catch (Exception e) { if(path.toLowerCase().contains("measure")) { - System.out.println("Error reading in Measure from path: " + path + "\n" + e); + logger.error("Error reading in Measure from path: {} \n {}", path, e); } } } @@ -814,28 +821,27 @@ private static void setupMeasurePaths(FhirContext fhirContext) { RuntimeResourceDefinition measureDefinition = ResourceUtils.getResourceDefinition(fhirContext, "Measure"); String measureClassName = measureDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> measureClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> { - measurePaths.add(entry.getKey()); - measures.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); - measurePathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); - }); + .filter(entry -> entry.getValue() != null) + .filter(entry -> measureClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> { + measurePaths.add(entry.getKey()); + measures.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); + measurePathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); + }); } } - private static HashSet<String> measureReportPaths = new LinkedHashSet<String>(); - public static HashSet<String> getMeasureReportPaths(FhirContext fhirContext) { + private static final HashSet<String> measureReportPaths = new LinkedHashSet<>(); + public static Set<String> getMeasureReportPaths(FhirContext fhirContext) { if (measureReportPaths.isEmpty()) { setupMeasureReportPaths(fhirContext); } return measureReportPaths; } private static void setupMeasureReportPaths(FhirContext fhirContext) { - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { @@ -846,27 +852,27 @@ private static void setupMeasureReportPaths(FhirContext fhirContext) { RuntimeResourceDefinition measureReportDefinition = ResourceUtils.getResourceDefinition(fhirContext, "MeasureReport"); String measureReportClassName = measureReportDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> measureReportClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> measureReportPaths.add(entry.getKey())); + .filter(entry -> entry.getValue() != null) + .filter(entry -> measureReportClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> measureReportPaths.add(entry.getKey())); } } - private static HashSet<String> planDefinitionPaths = new LinkedHashSet<String>(); - public static HashSet<String> getPlanDefinitionPaths(FhirContext fhirContext) { + private static final HashSet<String> planDefinitionPaths = new LinkedHashSet<>(); + public static Set<String> getPlanDefinitionPaths(FhirContext fhirContext) { if (planDefinitionPaths.isEmpty()) { setupPlanDefinitionPaths(fhirContext); } return planDefinitionPaths; } - private static Map<String, String> planDefinitionPathMap = new LinkedHashMap<String, String>(); + private static final Map<String, String> planDefinitionPathMap = new LinkedHashMap<>(); public static Map<String, String> getPlanDefinitionPathMap(FhirContext fhirContext) { if (planDefinitionPathMap.isEmpty()) { setupPlanDefinitionPaths(fhirContext); } return planDefinitionPathMap; } - private static Map<String, IBaseResource> planDefinitions = new LinkedHashMap<String, IBaseResource>(); + private static final Map<String, IBaseResource> planDefinitions = new LinkedHashMap<>(); public static Map<String, IBaseResource> getPlanDefinitions(FhirContext fhirContext) { if (planDefinitions.isEmpty()) { setupPlanDefinitionPaths(fhirContext); @@ -874,38 +880,37 @@ public static Map<String, IBaseResource> getPlanDefinitions(FhirContext fhirCont return planDefinitions; } private static void setupPlanDefinitionPaths(FhirContext fhirContext) { - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { - System.out.println(String.format("Error setting PlanDefinition paths while reading resource at: '%s'. Error: %s", path, e.getMessage())); + logger.error("Error setting PlanDefinition paths while reading resource at: {}. Error: {}", path, e.getMessage()); } } RuntimeResourceDefinition planDefinitionDefinition = ResourceUtils.getResourceDefinition(fhirContext, "PlanDefinition"); String planDefinitionClassName = planDefinitionDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> planDefinitionClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> { - planDefinitionPaths.add(entry.getKey()); - planDefinitions.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); - planDefinitionPathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); - }); + .filter(entry -> entry.getValue() != null) + .filter(entry -> planDefinitionClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> { + planDefinitionPaths.add(entry.getKey()); + planDefinitions.put(entry.getValue().getIdElement().getIdPart(), entry.getValue()); + planDefinitionPathMap.put(entry.getValue().getIdElement().getIdPart(), entry.getKey()); + }); } } - private static HashSet<String> questionnairePaths = new LinkedHashSet<String>(); - public static HashSet<String> getQuestionnairePaths(FhirContext fhirContext) { + private static final HashSet<String> questionnairePaths = new LinkedHashSet<>(); + public static Set<String> getQuestionnairePaths(FhirContext fhirContext) { if (questionnairePaths.isEmpty()) { setupQuestionnairePaths(fhirContext); } return questionnairePaths; } - private static Map<String, String> questionnairePathMap = new LinkedHashMap<String, String>(); + private static final Map<String, String> questionnairePathMap = new LinkedHashMap<>(); public static Map<String, String> getQuestionnairePathMap(FhirContext fhirContext) { if (questionnairePathMap.isEmpty()) { setupQuestionnairePaths(fhirContext); @@ -913,7 +918,7 @@ public static Map<String, String> getQuestionnairePathMap(FhirContext fhirContex return questionnairePathMap; } - private static Map<String, IBaseResource> questionnaires = new LinkedHashMap<String, IBaseResource>(); + private static final Map<String, IBaseResource> questionnaires = new LinkedHashMap<>(); public static Map<String, IBaseResource> getQuestionnaires(FhirContext fhirContext) { if (questionnaires.isEmpty()) { setupQuestionnairePaths(fhirContext); @@ -922,14 +927,13 @@ public static Map<String, IBaseResource> getQuestionnaires(FhirContext fhirConte } private static void setupQuestionnairePaths(FhirContext fhirContext) { - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { - System.out.println(String.format("Error setting Questionnaire paths while reading resource at: '%s'. Error: %s", path, e.getMessage())); + logger.error("Error setting Questionnaire paths while reading resource at: {}. Error: {}", path, e.getMessage()); } } RuntimeResourceDefinition questionnaireDefinition = ResourceUtils.getResourceDefinition(fhirContext, "Questionnaire"); @@ -945,23 +949,22 @@ private static void setupQuestionnairePaths(FhirContext fhirContext) { } } - private static HashSet<String> activityDefinitionPaths = new LinkedHashSet<String>(); - public static HashSet<String> getActivityDefinitionPaths(FhirContext fhirContext) { + private static final HashSet<String> activityDefinitionPaths = new LinkedHashSet<>(); + public static Set<String> getActivityDefinitionPaths(FhirContext fhirContext) { if (activityDefinitionPaths.isEmpty()) { - System.out.println("Reading activitydefinitions"); + logger.info("Reading activitydefinitions"); setupActivityDefinitionPaths(fhirContext); } return activityDefinitionPaths; } private static void setupActivityDefinitionPaths(FhirContext fhirContext) { - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); // BUG: resourceDirectories is being populated with all "per-convention" directories during validation. So, // if you have resources in the /tests directory for example, they will be picked up from there, rather than // from your resources directories. - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { @@ -971,25 +974,23 @@ private static void setupActivityDefinitionPaths(FhirContext fhirContext) { RuntimeResourceDefinition activityDefinitionDefinition = ResourceUtils.getResourceDefinition(fhirContext, "ActivityDefinition"); String activityDefinitionClassName = activityDefinitionDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> activityDefinitionClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> activityDefinitionPaths.add(entry.getKey())); + .filter(entry -> entry.getValue() != null) + .filter(entry -> activityDefinitionClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> activityDefinitionPaths.add(entry.getKey())); } } - public static void ensurePath(String path) throws IOException { + public static void ensurePath(String path) { //Creating a File object File scopeDir = new File(path); //Creating the directory - if (!scopeDir.exists()) { - if (!scopeDir.mkdirs()) { - throw new IOException("Could not create directory: " + path); - } + if (!scopeDir.exists() && !scopeDir.mkdirs()) { + throw new IllegalArgumentException("Could not create directory: " + path); } } private static HashSet<String> devicePaths; - public static HashSet<String> getDevicePaths(FhirContext fhirContext) { + public static Set<String> getDevicePaths(FhirContext fhirContext) { if (devicePaths == null) { setupDevicePaths(fhirContext); } @@ -1002,16 +1003,15 @@ public static void clearDevicePaths() { } private static void setupDevicePaths(FhirContext fhirContext) { - devicePaths = new LinkedHashSet<String>(); - HashMap<String, IBaseResource> resources = new LinkedHashMap<String, IBaseResource>(); - for(String dir : resourceDirectories) { - for(String path : IOUtils.getFilePaths(dir, true)) - { + devicePaths = new LinkedHashSet<>(); + HashMap<String, IBaseResource> resources = new LinkedHashMap<>(); + for (String dir : resourceDirectories) { + for(String path : IOUtils.getFilePaths(dir, true)) { try { resources.put(path, IOUtils.readResource(path, fhirContext, true)); } catch (Exception e) { if(path.toLowerCase().contains("device")) { - System.out.println("Error reading in Device from path: " + path + "\n" + e); + logger.error("Error reading in Device from path: {} \n {}", path, e); } } } @@ -1019,20 +1019,20 @@ private static void setupDevicePaths(FhirContext fhirContext) { RuntimeResourceDefinition deviceDefinition = ResourceUtils.getResourceDefinition(fhirContext, "Device"); String deviceClassName = deviceDefinition.getImplementingClass().getName(); resources.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .filter(entry -> deviceClassName.equals(entry.getValue().getClass().getName())) - .forEach(entry -> devicePaths.add(entry.getKey())); + .filter(entry -> entry.getValue() != null) + .filter(entry -> deviceClassName.equals(entry.getValue().getClass().getName())) + .forEach(entry -> devicePaths.add(entry.getKey())); } } public static boolean isXMLOrJson(String fileDirPath, String libraryName){ String fileExtension = libraryName.substring(libraryName.lastIndexOf(".") + 1); - if(fileExtension.equalsIgnoreCase("xml") || + if (fileExtension.equalsIgnoreCase("xml") || fileExtension.equalsIgnoreCase("json")){ return true; } - System.out.println("The file " + fileDirPath + libraryName + " is not the right type of file."); + logger.warn("The file {}{} is not the right type of file.", fileDirPath, libraryName); return false; } -} +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogUtils.java index 8deb907ac..194850862 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogUtils.java @@ -9,16 +9,18 @@ public class LogUtils { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LogUtils.class); - private static final Map<String, String> resourceWarnings = new LinkedHashMap<String, String>(); + private static final Map<String, String> resourceWarnings = new LinkedHashMap<>(); + + private LogUtils() {} public static void putException(String id, Exception e) { e.printStackTrace(); - resourceWarnings.put(LocalDateTime.now().toString() + ": " + id, + resourceWarnings.put(LocalDateTime.now() + ": " + id, e.getMessage() == null ? e.toString() : e.getMessage()); } public static void putException(String id, String warning) { - resourceWarnings.put(LocalDateTime.now().toString() + ": " + id, warning); + resourceWarnings.put(LocalDateTime.now() + ": " + id, warning); } public static void info(String message) { @@ -43,14 +45,14 @@ public static void warn(String libraryName) { if (resourceWarnings.isEmpty()) { return; } - String exceptionMessage = ""; + StringBuilder exceptionMessage = new StringBuilder(); for (Map.Entry<String, String> resourceException : resourceWarnings.entrySet()) { - String resourceExceptionMessage = truncateMessage(resourceException.getValue()); - String resource = FilenameUtils.getBaseName(stripTimestamp(resourceException.getKey())); - exceptionMessage += "\r\n Resource could not be processed: " + resource - + "\r\n " + resourceExceptionMessage; + exceptionMessage.append("\r\n Resource could not be processed: "); + exceptionMessage.append(FilenameUtils.getBaseName(stripTimestamp(resourceException.getKey()))); + exceptionMessage.append("\r\n "); + exceptionMessage.append(truncateMessage(resourceException.getValue())); } - ourLog.warn(libraryName + " could not be processed: " + exceptionMessage); + ourLog.warn("{} could not be processed: {}", libraryName, exceptionMessage); resourceWarnings.clear(); } @@ -63,7 +65,7 @@ private static String truncateMessage(String message) { int cutoffIndex = 0; for (String string : messages) { int stringIndex = cutoffIndex + string.length() + 4; - cutoffIndex = stringIndex > maxSize ? maxSize : stringIndex; + cutoffIndex = Math.min(stringIndex, maxSize); if (cutoffIndex == maxSize) { break; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ModelCanonicalAtlasCreator.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ModelCanonicalAtlasCreator.java index d47b7a95e..af1001be0 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ModelCanonicalAtlasCreator.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ModelCanonicalAtlasCreator.java @@ -13,12 +13,13 @@ import java.util.Map; public class ModelCanonicalAtlasCreator { - private static List<ValueSet> valueSets; private static List<CodeSystem> codeSystems; private static List<StructureDefinition> structureDefinitions; private static Map<String, ConceptMap> conceptMaps; + private ModelCanonicalAtlasCreator() {} + public static CanonicalResourceAtlas createMainCanonicalAtlas (String resourcePaths, String modelName, String modelVersion, String inputPath) { String mainResourcePath = getModelResourcePath (resourcePaths, modelName); setSystems (inputPath, mainResourcePath); @@ -34,18 +35,12 @@ private static void setSystems (String inputPath, String resourcePath) { Atlas atlas = new Atlas (); atlas.loadPaths (inputPath, resourcePath); codeSystems = new ArrayList<> (); - atlas.getCodeSystems().forEach((key, codeSystem)->{ - codeSystems.add(codeSystem); - }); + atlas.getCodeSystems().forEach((key, codeSystem) -> codeSystems.add(codeSystem)); conceptMaps = atlas.getConceptMaps(); valueSets = new ArrayList<>(); - atlas.getValueSets().forEach((key, valueSet)->{ - valueSets.add(valueSet); - }); + atlas.getValueSets().forEach((key, valueSet) -> valueSets.add(valueSet)); structureDefinitions = new ArrayList<>(); - atlas.getStructureDefinitions().forEach((key, structureDefinition)->{ - structureDefinitions.add(structureDefinition); - }); + atlas.getStructureDefinitions().forEach((key, structureDefinition) -> structureDefinitions.add(structureDefinition)); } private static CanonicalResourceAtlas getCanonicalAtlas(){ @@ -70,7 +65,7 @@ private static String getDependenciesResourcePath(String resourcePaths, String m StringBuilder pathsWithoutModel = new StringBuilder(); for (String path : paths){ if(!path.contains(modelName)){ - pathsWithoutModel.append(path + ";"); + pathsWithoutModel.append(path).append(";"); } } return pathsWithoutModel.toString(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/NpmUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/NpmUtils.java new file mode 100644 index 000000000..0461ce063 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/NpmUtils.java @@ -0,0 +1,51 @@ +package org.opencds.cqf.tooling.utilities; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; + +public class NpmUtils { + + private NpmUtils() {} + + public static class PackageLoaderValidationSupport extends PrePopulatedValidationSupport { + + public PackageLoaderValidationSupport(@NotNull FhirContext fhirContext) { + super(fhirContext); + } + + public void loadPackage(NpmPackage npmPackage) throws IOException { + if (npmPackage.getFolders().containsKey("package")) { + loadResourcesFromPackage(npmPackage); + loadBinariesFromPackage(npmPackage); + } + } + + private void loadResourcesFromPackage(NpmPackage thePackage) { + NpmPackage.NpmPackageFolder packageFolder = thePackage.getFolders().get("package"); + + for (String nextFile : packageFolder.listFiles()) { + if (nextFile.toLowerCase(Locale.US).endsWith(".json")) { + String input = new String(packageFolder.getContent().get(nextFile), StandardCharsets.UTF_8); + IBaseResource resource = getFhirContext().newJsonParser().parseResource(input); + super.addResource(resource); + } + } + } + + private void loadBinariesFromPackage(NpmPackage thePackage) throws IOException { + List<String> binaries = thePackage.list("other"); + for (String binaryName : binaries) { + addBinary(TextFile.streamToBytes(thePackage.load("other", binaryName)), binaryName); + } + } + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/OperationUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/OperationUtils.java new file mode 100644 index 000000000..6633c2acc --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/OperationUtils.java @@ -0,0 +1,79 @@ +package org.opencds.cqf.tooling.utilities; + +import com.jakewharton.fliptables.FlipTable; +import org.apache.commons.lang3.ArrayUtils; +import org.opencds.cqf.tooling.exception.InvalidOperationArgs; +import org.opencds.cqf.tooling.operations.ExecutableOperation; +import org.opencds.cqf.tooling.operations.OperationParam; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; + +public class OperationUtils { + private static final String[] HELP_ARGS = new String[] { "-h", "-help", "-?" }; + + private OperationUtils() {} + + public static Class<?> getParamType(ExecutableOperation operation, String methodName) { + for (Method m : operation.getClass().getDeclaredMethods()) { + if (m.getName().equals(methodName)) { + if (m.getParameterCount() > 1) { + continue; + } + return m.getParameterTypes()[0]; + } + } + throw new InvalidOperationArgs(String.format( + "Unable to find setter method for %s with a single parameter", methodName)); + } + + // Parameter types currently supported: String, Integer, Boolean + public static <T> T mapParamType(String value, Class<T> clazz) { + if (clazz.isAssignableFrom(value.getClass())) { + return clazz.cast(value); + } + + if (clazz.isAssignableFrom(Integer.class)) { + return clazz.cast(Integer.decode(value)); + } else if (clazz.isAssignableFrom(Boolean.class)) { + return clazz.cast(Boolean.valueOf(value)); + } + + throw new InvalidOperationArgs( + "Operation parameters are not currently supported for type: " + clazz.getSimpleName()); + } + + public static String getHelpMenu(ExecutableOperation operation) { + String[] headers = new String[]{ "Parameter", "Description" }; + String[][] rows = new String[getOperationParamCount(operation)][2]; + int idx = 0; + for (Field field : operation.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(OperationParam.class)) { + rows[idx][0] = formatAliases(field.getAnnotation(OperationParam.class).alias()); + rows[idx++][1] = field.getAnnotation(OperationParam.class).description(); + } + } + + return System.lineSeparator() + FlipTable.of(headers, rows); + } + + public static int getOperationParamCount(ExecutableOperation operation) { + int count = 0; + for (Field field : operation.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(OperationParam.class)) { + ++count; + } + } + return count; + } + + public static String formatAliases(String[] aliases) { + return Arrays.toString(aliases).replace("[", "-") + .replace(", ", " | -").replace("]", ""); + } + + public static boolean isHelpArg(String arg) { + return ArrayUtils.contains(HELP_ARGS, arg); + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/R4FHIRUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/R4FHIRUtils.java index d9c19f357..6204f7930 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/R4FHIRUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/R4FHIRUtils.java @@ -21,6 +21,8 @@ public class R4FHIRUtils { + private R4FHIRUtils() {} + public static Coding toCoding(Code code, CompiledLibrary library, LibraryManager libraryManager) { CodeSystemDef codeSystemDef = resolveCodeSystemRef(code.getSystem(), library, libraryManager); Coding coding = new Coding(); @@ -49,7 +51,7 @@ public static String toReference(ValueSetDef valueSetDef) { } public static String parseId(String reference) { - String[] tokens = reference.split("[/]"); + String[] tokens = reference.split("/"); if (tokens.length > 1) { return tokens[1]; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java index d9b4a8d08..8e307ef5a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java @@ -7,12 +7,16 @@ import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import ca.uhn.fhir.util.TerserUtil; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.Validate; import org.cqframework.cql.cql2elm.CqlTranslator; @@ -24,6 +28,7 @@ import org.cqframework.cql.cql2elm.quick.FhirLibrarySourceProvider; import org.hl7.elm.r1.IncludeDef; import org.hl7.elm.r1.ValueSetDef; +import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseElement; @@ -45,259 +50,255 @@ import ca.uhn.fhir.context.RuntimeCompositeDatatypeDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; -public class ResourceUtils -{ - private static final Logger logger = LoggerFactory.getLogger(ResourceUtils.class); - - private static String cqfLibraryExtensionUrl = "http://hl7.org/fhir/StructureDefinition/cqf-library"; - - public enum FhirVersion - { - DSTU3("dstu3"), R4("r4"); - - private String string; - public String toString() - { - return this.string; - } - - private FhirVersion(String string) - { - this.string = string; - } - - public static FhirVersion parse(String value) { - switch (value) { - case "dstu3": - return DSTU3; - case "r4": - return R4; - default: - throw new RuntimeException("Unable to parse FHIR version value:" + value); - } - } - } - - public static String getId(String name, String version, boolean versioned) { - return name.replaceAll("_", "-") + (versioned ? "-" + version.replaceAll("_", ".") : ""); - } - - public static void setIgId(String baseId, IBaseResource resource, Boolean includeVersion) - { - String version = includeVersion ? resource.getMeta().getVersionId() : ""; +public class ResourceUtils { + private static final Logger logger = LoggerFactory.getLogger(ResourceUtils.class); + private static final String CQF_LIBRARY_EXT_URL = "http://hl7.org/fhir/StructureDefinition/cqf-library"; + + public enum FhirVersion { + DSTU3("dstu3"), R4("r4"); + + private String string; + public String toString() + { + return this.string; + } + + private FhirVersion(String string) + { + this.string = string; + } + + public static FhirVersion parse(String value) { + switch (value) { + case "dstu3": + return DSTU3; + case "r4": + return R4; + default: + throw new RuntimeException("Unable to parse FHIR version value:" + value); + } + } + } + + public static String getId(String name, String version, boolean versioned) { + return name.replace("_", "-") + (versioned ? "-" + version.replace("_", ".") : ""); + } + + public static void setIgId(String baseId, IBaseResource resource, Boolean includeVersion) + { + String version = Boolean.TRUE.equals(includeVersion) ? resource.getMeta().getVersionId() : ""; setIgId(baseId, resource, version); - } + } - public static void setIgId(String baseId, IBaseResource resource, String version) - { + public static void setIgId(String baseId, IBaseResource resource, String version) + { String igId = ""; String resourceName = resource.getClass().getSimpleName().toLowerCase(); String versionId = (version == null || version.equals("")) ? "" : "-" + version; if (resource instanceof org.hl7.fhir.dstu3.model.Bundle || resource instanceof org.hl7.fhir.r4.model.Bundle) { - igId = baseId + versionId + "-" + resourceName; + igId = baseId + versionId + "-" + resourceName; } else { - igId = resourceName + "-" + baseId + versionId; + igId = resourceName + "-" + baseId + versionId; } igId = igId.replace("_", "-"); resource.setId(igId); - } + } - public static FhirContext getFhirContext(FhirVersion fhirVersion) { + public static FhirContext getFhirContext(FhirVersion fhirVersion) { switch (fhirVersion) { - case DSTU3: - return FhirContext.forDstu3Cached(); - case R4: - return FhirContext.forR4Cached(); - default: - throw new IllegalArgumentException("Unsupported FHIR version: " + fhirVersion); + case DSTU3: + return FhirContext.forDstu3Cached(); + case R4: + return FhirContext.forR4Cached(); + default: + throw new IllegalArgumentException("Unsupported FHIR version: " + fhirVersion); } - } + } - private static List<org.hl7.fhir.dstu3.model.RelatedArtifact> getStu3RelatedArtifacts(String pathToLibrary, FhirContext fhirContext) { + private static List<org.hl7.fhir.dstu3.model.RelatedArtifact> getStu3RelatedArtifacts(String pathToLibrary, FhirContext fhirContext) { Object mainLibrary = IOUtils.readResource(pathToLibrary, fhirContext); if (!(mainLibrary instanceof org.hl7.fhir.dstu3.model.Library)) { - throw new IllegalArgumentException("pathToLibrary must be a path to a Library type Resource"); + throw new IllegalArgumentException("pathToLibrary must be a path to a Library type Resource"); } return ((org.hl7.fhir.dstu3.model.Library)mainLibrary).getRelatedArtifact(); - } + } - private static List<org.hl7.fhir.r4.model.RelatedArtifact> getR4RelatedArtifacts(String pathToLibrary, FhirContext fhirContext) { + private static List<org.hl7.fhir.r4.model.RelatedArtifact> getR4RelatedArtifacts(String pathToLibrary, FhirContext fhirContext) { Object mainLibrary = IOUtils.readResource(pathToLibrary, fhirContext); if (!(mainLibrary instanceof org.hl7.fhir.r4.model.Library)) { - throw new IllegalArgumentException("pathToLibrary must be a path to a Library type Resource"); + throw new IllegalArgumentException("pathToLibrary must be a path to a Library type Resource"); } return ((org.hl7.fhir.r4.model.Library)mainLibrary).getRelatedArtifact(); - } + } - public static Map<String, IBaseResource> getDepLibraryResources(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned, Logger logger) { - Map<String, IBaseResource> dependencyLibraries = new HashMap<String, IBaseResource>(); + public static Map<String, IBaseResource> getDepLibraryResources(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned, Logger logger) { + Map<String, IBaseResource> dependencyLibraries = new HashMap<>(); switch (fhirContext.getVersion().getVersion()) { - case DSTU3: + case DSTU3: return getStu3DepLibraryResources(path, dependencyLibraries, fhirContext, encoding, versioned); - case R4: + case R4: return getR4DepLibraryResources(path, dependencyLibraries, fhirContext, encoding, versioned, logger); - default: + default: throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); } - } + } - public static List<String> getDepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { + public static List<String> getDepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { switch (fhirContext.getVersion().getVersion()) { - case DSTU3: + case DSTU3: return getStu3DepLibraryPaths(path, fhirContext, encoding, versioned); - case R4: + case R4: return getR4DepLibraryPaths(path, fhirContext, encoding,versioned); - default: + default: throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); } - } + } - private static List<String> getStu3DepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { - List<String> paths = new ArrayList<String>(); + private static List<String> getStu3DepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { + List<String> paths = new ArrayList<>(); String directoryPath = FilenameUtils.getFullPath(path); String fileName = FilenameUtils.getName(path); String prefix = fileName.toLowerCase().startsWith("library-") ? fileName.substring(0, 8) : ""; List<org.hl7.fhir.dstu3.model.RelatedArtifact> relatedArtifacts = getStu3RelatedArtifacts(path, fhirContext); for (org.hl7.fhir.dstu3.model.RelatedArtifact relatedArtifact : relatedArtifacts) { - if (relatedArtifact.getType() == org.hl7.fhir.dstu3.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { + if (relatedArtifact.getType() == org.hl7.fhir.dstu3.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { if (relatedArtifact.getResource().getReference().contains("Library/")) { - String dependencyLibraryName; - // Issue 96 - Do not include version number in the filename - if (versioned) { - dependencyLibraryName = IOUtils.formatFileName(relatedArtifact.getResource().getReference().split("Library/")[1].replaceAll("\\|", "-"), encoding, fhirContext); - } else { - String name = relatedArtifact.getResource().getReference().split("Library/")[1]; - dependencyLibraryName = IOUtils.formatFileName(name.split("\\|")[0], encoding, fhirContext); - } - String dependencyLibraryPath = FilenameUtils.concat(directoryPath, prefix + dependencyLibraryName); - IOUtils.putAllInListIfAbsent(getStu3DepLibraryPaths(dependencyLibraryPath, fhirContext, encoding, versioned), paths); - IOUtils.putInListIfAbsent(dependencyLibraryPath, paths); - } - } + String dependencyLibraryName; + // Issue 96 - Do not include version number in the filename + if (Boolean.TRUE.equals(versioned)) { + dependencyLibraryName = IOUtils.formatFileName(relatedArtifact.getResource().getReference().split("Library/")[1].replace("\\|", "-"), encoding, fhirContext); + } else { + String name = relatedArtifact.getResource().getReference().split("Library/")[1]; + dependencyLibraryName = IOUtils.formatFileName(name.split("\\|")[0], encoding, fhirContext); + } + String dependencyLibraryPath = FilenameUtils.concat(directoryPath, prefix + dependencyLibraryName); + IOUtils.putAllInListIfAbsent(getStu3DepLibraryPaths(dependencyLibraryPath, fhirContext, encoding, versioned), paths); + IOUtils.putInListIfAbsent(dependencyLibraryPath, paths); + } + } } return paths; - } + } - private static Map<String, IBaseResource> getStu3DepLibraryResources(String path, Map<String, IBaseResource> dependencyLibraries, FhirContext fhirContext, Encoding encoding, Boolean versioned) { + private static Map<String, IBaseResource> getStu3DepLibraryResources(String path, Map<String, IBaseResource> dependencyLibraries, FhirContext fhirContext, Encoding encoding, Boolean versioned) { List<String> dependencyLibraryPaths = getStu3DepLibraryPaths(path, fhirContext, encoding, versioned); for (String dependencyLibraryPath : dependencyLibraryPaths) { - Object resource = IOUtils.readResource(dependencyLibraryPath, fhirContext); - if (resource instanceof org.hl7.fhir.dstu3.model.Library) { - org.hl7.fhir.dstu3.model.Library library = (org.hl7.fhir.dstu3.model.Library)resource; - dependencyLibraries.putIfAbsent(library.getId(), library); - } + Object resource = IOUtils.readResource(dependencyLibraryPath, fhirContext); + if (resource instanceof org.hl7.fhir.dstu3.model.Library) { + org.hl7.fhir.dstu3.model.Library library = (org.hl7.fhir.dstu3.model.Library)resource; + dependencyLibraries.putIfAbsent(library.getId(), library); + } } return dependencyLibraries; - } + } - // if | exists there is a version - private static List<String> getR4DepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { - List<String> paths = new ArrayList<String>(); + // if | exists there is a version + private static List<String> getR4DepLibraryPaths(String path, FhirContext fhirContext, Encoding encoding, Boolean versioned) { + List<String> paths = new ArrayList<>(); String directoryPath = FilenameUtils.getFullPath(path); String fileName = FilenameUtils.getName(path); String prefix = fileName.toLowerCase().startsWith("library-") ? fileName.substring(0, 8) : ""; List<org.hl7.fhir.r4.model.RelatedArtifact> relatedArtifacts = getR4RelatedArtifacts(path, fhirContext); for (org.hl7.fhir.r4.model.RelatedArtifact relatedArtifact : relatedArtifacts) { - if (relatedArtifact.getType() == org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { - if (relatedArtifact.getResource().contains("Library/")) { - String dependencyLibraryName; - // Issue 96 - Do not include version number in the filename - if (versioned) { - dependencyLibraryName = IOUtils.formatFileName(relatedArtifact.getResource().split("Library/")[1].replaceAll("\\|", "-"), encoding, fhirContext); - } else { - String name = relatedArtifact.getResource().split("Library/")[1]; - dependencyLibraryName = IOUtils.formatFileName(name.split("\\|")[0], encoding, fhirContext); - } - String dependencyLibraryPath = FilenameUtils.concat(directoryPath, prefix + dependencyLibraryName); - IOUtils.putInListIfAbsent(dependencyLibraryPath, paths); - } - } + if (relatedArtifact.getType() == org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { + if (relatedArtifact.getResource().contains("Library/")) { + String dependencyLibraryName; + // Issue 96 - Do not include version number in the filename + if (versioned) { + dependencyLibraryName = IOUtils.formatFileName(relatedArtifact.getResource().split("Library/")[1].replaceAll("\\|", "-"), encoding, fhirContext); + } else { + String name = relatedArtifact.getResource().split("Library/")[1]; + dependencyLibraryName = IOUtils.formatFileName(name.split("\\|")[0], encoding, fhirContext); + } + String dependencyLibraryPath = FilenameUtils.concat(directoryPath, prefix + dependencyLibraryName); + IOUtils.putInListIfAbsent(dependencyLibraryPath, paths); + } + } } return paths; - } + } - private static Map<String, IBaseResource> getR4DepLibraryResources(String path, Map<String, IBaseResource> dependencyLibraries, FhirContext fhirContext, Encoding encoding, Boolean versioned, Logger logger) { + private static Map<String, IBaseResource> getR4DepLibraryResources(String path, Map<String, IBaseResource> dependencyLibraries, FhirContext fhirContext, Encoding encoding, Boolean versioned, Logger logger) { List<String> dependencyLibraryPaths = getR4DepLibraryPaths(path, fhirContext, encoding, versioned); for (String dependencyLibraryPath : dependencyLibraryPaths) { - if (dependencyLibraryPath.contains("ModelInfo")) { - logger.debug("skipping ModelInfo"); - } else { - Object resource = IOUtils.readResource(dependencyLibraryPath, fhirContext); - if (resource instanceof org.hl7.fhir.r4.model.Library) { - org.hl7.fhir.r4.model.Library library = (org.hl7.fhir.r4.model.Library)resource; - dependencyLibraries.putIfAbsent(library.getId(), library); - } - } + if (dependencyLibraryPath.contains("ModelInfo")) { + logger.debug("skipping ModelInfo"); + } else { + Object resource = IOUtils.readResource(dependencyLibraryPath, fhirContext); + if (resource instanceof org.hl7.fhir.r4.model.Library) { + org.hl7.fhir.r4.model.Library library = (org.hl7.fhir.r4.model.Library)resource; + dependencyLibraries.putIfAbsent(library.getId(), library); + } + } } return dependencyLibraries; - } - - public static List<String> getStu3TerminologyDependencies(List<org.hl7.fhir.dstu3.model.RelatedArtifact> relatedArtifacts) { - List<String> urls = new ArrayList<String>(); - for (org.hl7.fhir.dstu3.model.RelatedArtifact relatedArtifact : relatedArtifacts) { - if (relatedArtifact.hasType() && relatedArtifact.getType() == org.hl7.fhir.dstu3.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { - if (relatedArtifact.hasResource() && relatedArtifact.getResource().hasReference() - && (relatedArtifact.getResource().getReference().contains("CodeSystem/") || relatedArtifact.getResource().getReference().contains("ValueSet/"))) { - urls.add(relatedArtifact.getResource().getReference()); - } - } - } - return urls; - } - - public static List<String> getR4TerminologyDependencies(List<org.hl7.fhir.r4.model.RelatedArtifact> relatedArtifacts) { - List<String> urls = new ArrayList<String>(); - for (org.hl7.fhir.r4.model.RelatedArtifact relatedArtifact : relatedArtifacts) { - if (relatedArtifact.hasType() && relatedArtifact.getType() == org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { - if (relatedArtifact.hasResource() && (relatedArtifact.getResource().contains("CodeSystem/") || relatedArtifact.getResource().contains("ValueSet/"))) { - urls.add(relatedArtifact.getResource()); - } - } - } - return urls; - } - - public static List<String> getTerminologyDependencies(IBaseResource resource, FhirContext fhirContext) throws Exception { - switch (fhirContext.getVersion().getVersion()) { - case DSTU3: - switch (resource.fhirType()) { - case "Library": { - return getStu3TerminologyDependencies(((org.hl7.fhir.dstu3.model.Library)resource).getRelatedArtifact()); - } - case "Measure": { - return getStu3TerminologyDependencies(((org.hl7.fhir.dstu3.model.Measure)resource).getRelatedArtifact()); - } - default: throw new IllegalArgumentException(String.format("Could not retrieve relatedArtifacts from %s", resource.fhirType())); - } - case R4: - switch (resource.fhirType()) { - case "Library": { - return getR4TerminologyDependencies(((org.hl7.fhir.r4.model.Library)resource).getRelatedArtifact()); - } - case "Measure": { - return getR4TerminologyDependencies(((org.hl7.fhir.r4.model.Measure)resource).getRelatedArtifact()); - } - default: throw new IllegalArgumentException(String.format("Could not retrieve relatedArtifacts from %s", resource.fhirType())); - } - default: - throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); - } - } + } - public static Map<String, IBaseResource> getDepValueSetResources(String cqlContentPath, String igPath, FhirContext fhirContext, boolean includeDependencies, Boolean includeVersion) throws Exception { - Map<String, IBaseResource> valueSetResources = new HashMap<String, IBaseResource>(); - List<String> valueSetDefIDs = getDepELMValueSetDefIDs(cqlContentPath); - HashSet<String> dependencies = new HashSet<>(); + public static List<String> getStu3TerminologyDependencies(List<org.hl7.fhir.dstu3.model.RelatedArtifact> relatedArtifacts) { + List<String> urls = new ArrayList<>(); + for (org.hl7.fhir.dstu3.model.RelatedArtifact relatedArtifact : relatedArtifacts) { + if (relatedArtifact.hasType() && relatedArtifact.getType() == org.hl7.fhir.dstu3.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { + if (relatedArtifact.hasResource() && relatedArtifact.getResource().hasReference() + && (relatedArtifact.getResource().getReference().contains("CodeSystem/") || relatedArtifact.getResource().getReference().contains("ValueSet/"))) { + urls.add(relatedArtifact.getResource().getReference()); + } + } + } + return urls; + } + + public static List<String> getR4TerminologyDependencies(List<org.hl7.fhir.r4.model.RelatedArtifact> relatedArtifacts) { + List<String> urls = new ArrayList<>(); + for (org.hl7.fhir.r4.model.RelatedArtifact relatedArtifact : relatedArtifacts) { + if (relatedArtifact.hasType() && relatedArtifact.getType() == org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType.DEPENDSON) { + if (relatedArtifact.hasResource() && (relatedArtifact.getResource().contains("CodeSystem/") || relatedArtifact.getResource().contains("ValueSet/"))) { + urls.add(relatedArtifact.getResource()); + } + } + } + return urls; + } + + public static List<String> getTerminologyDependencies(IBaseResource resource, FhirContext fhirContext) { + switch (fhirContext.getVersion().getVersion()) { + case DSTU3: + switch (resource.fhirType()) { + case "Library": { + return getStu3TerminologyDependencies(((org.hl7.fhir.dstu3.model.Library)resource).getRelatedArtifact()); + } + case "Measure": { + return getStu3TerminologyDependencies(((org.hl7.fhir.dstu3.model.Measure)resource).getRelatedArtifact()); + } + default: throw new IllegalArgumentException(String.format("Could not retrieve relatedArtifacts from %s", resource.fhirType())); + } + case R4: + switch (resource.fhirType()) { + case "Library": { + return getR4TerminologyDependencies(((org.hl7.fhir.r4.model.Library)resource).getRelatedArtifact()); + } + case "Measure": { + return getR4TerminologyDependencies(((org.hl7.fhir.r4.model.Measure)resource).getRelatedArtifact()); + } + default: throw new IllegalArgumentException(String.format("Could not retrieve relatedArtifacts from %s", resource.fhirType())); + } + default: + throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static Map<String, IBaseResource> getDepValueSetResources(String cqlContentPath, String igPath, FhirContext fhirContext, boolean includeDependencies, Boolean includeVersion) throws Exception { + Map<String, IBaseResource> valueSetResources = new HashMap<>(); + List<String> valueSetDefIDs = getDepELMValueSetDefIDs(cqlContentPath); for (String valueSetUrl : valueSetDefIDs) { - ValueSetsProcessor.getCachedValueSets(fhirContext).entrySet().stream() - .filter(entry -> entry.getKey().equals(valueSetUrl)) - .forEach(entry -> valueSetResources.put(entry.getKey(), entry.getValue())); + ValueSetsProcessor.getCachedValueSets(fhirContext).entrySet().stream() + .filter(entry -> entry.getKey().equals(valueSetUrl)) + .forEach(entry -> valueSetResources.put(entry.getKey(), entry.getValue())); } - dependencies.addAll(valueSetDefIDs); + Set<String> dependencies = new HashSet<>(valueSetDefIDs); if (includeDependencies) { List<String> dependencyCqlPaths = IOUtils.getDependencyCqlPaths(cqlContentPath, includeVersion); @@ -305,102 +306,98 @@ public static Map<String, IBaseResource> getDepValueSetResources(String cqlConte Map<String, IBaseResource> dependencyValueSets = getDepValueSetResources(path, igPath, fhirContext, includeDependencies, includeVersion); dependencies.addAll(dependencyValueSets.keySet()); for (Entry<String, IBaseResource> entry : dependencyValueSets.entrySet()) { - valueSetResources.putIfAbsent(entry.getKey(), entry.getValue()); + valueSetResources.putIfAbsent(entry.getKey(), entry.getValue()); } } } if (dependencies.size() != valueSetResources.size()) { - String message = (dependencies.size() - valueSetResources.size()) + " missing ValueSets: \r\n"; - dependencies.removeAll(valueSetResources.keySet()); - for (String valueSetUrl : dependencies) { - message += valueSetUrl + " MISSING \r\n"; - } - System.out.println(message); - throw new Exception(message); + String message = (dependencies.size() - valueSetResources.size()) + " missing ValueSets: \r\n"; + dependencies.removeAll(valueSetResources.keySet()); + for (String valueSetUrl : dependencies) { + message += valueSetUrl + " MISSING \r\n"; + } + System.out.println(message); + throw new Exception(message); } return valueSetResources; - } + } - public static ArrayList<String> getIncludedLibraryNames(String cqlContentPath, Boolean includeVersion) { - ArrayList<String> includedLibraryNames = new ArrayList<String>(); - ArrayList<IncludeDef> includedDefs = getIncludedDefs(cqlContentPath); + public static List<String> getIncludedLibraryNames(String cqlContentPath, Boolean includeVersion) { + List<String> includedLibraryNames = new ArrayList<>(); + List<IncludeDef> includedDefs = getIncludedDefs(cqlContentPath); for (IncludeDef def : includedDefs) { - //TODO: replace true with versioned variable - IOUtils.putInListIfAbsent(getId(def.getPath(), def.getVersion(), includeVersion), includedLibraryNames); + //TODO: replace true with versioned variable + IOUtils.putInListIfAbsent(getId(def.getPath(), def.getVersion(), includeVersion), includedLibraryNames); } return includedLibraryNames; - } + } - public static ArrayList<String> getDepELMValueSetDefIDs(String cqlContentPath) { - ArrayList<String> includedValueSetDefIDs = new ArrayList<String>(); - ArrayList<ValueSetDef> valueSetDefs = getValueSetDefs(cqlContentPath); + public static List<String> getDepELMValueSetDefIDs(String cqlContentPath) { + List<String> includedValueSetDefIDs = new ArrayList<>(); + List<ValueSetDef> valueSetDefs = getValueSetDefs(cqlContentPath); for (ValueSetDef def : valueSetDefs) { - IOUtils.putInListIfAbsent(def.getId(), includedValueSetDefIDs); + IOUtils.putInListIfAbsent(def.getId(), includedValueSetDefIDs); } return includedValueSetDefIDs; - } + } - public static ArrayList<IncludeDef> getIncludedDefs(String cqlContentPath) { - ArrayList<IncludeDef> includedDefs = new ArrayList<IncludeDef>(); + public static List<IncludeDef> getIncludedDefs(String cqlContentPath) { + ArrayList<IncludeDef> includedDefs = new ArrayList<>(); org.hl7.elm.r1.Library elm; try { - elm = getElmFromCql(cqlContentPath); + elm = getElmFromCql(cqlContentPath); } catch (Exception e) { - System.out.println("error processing cql: "); - System.out.println(e.getMessage()); - return includedDefs; + System.out.println("error processing cql: "); + System.out.println(e.getMessage()); + return includedDefs; } if (elm.getIncludes() != null && !elm.getIncludes().getDef().isEmpty()) { - for (IncludeDef def : elm.getIncludes().getDef()) { - includedDefs.add(def); - } + includedDefs.addAll(elm.getIncludes().getDef()); } return includedDefs; - } + } - public static ArrayList<ValueSetDef> getValueSetDefs(String cqlContentPath) { - ArrayList<ValueSetDef> valueSetDefs = new ArrayList<ValueSetDef>(); + public static List<ValueSetDef> getValueSetDefs(String cqlContentPath) { + ArrayList<ValueSetDef> valueSetDefs = new ArrayList<>(); org.hl7.elm.r1.Library elm; try { - elm = getElmFromCql(cqlContentPath); + elm = getElmFromCql(cqlContentPath); } catch (Exception e) { - System.out.println("error translating cql: "); - return valueSetDefs; + System.out.println("error translating cql: "); + return valueSetDefs; } if (elm.getValueSets() != null && !elm.getValueSets().getDef().isEmpty()) { - for (ValueSetDef def : elm.getValueSets().getDef()) { - valueSetDefs.add(def); - } + valueSetDefs.addAll(elm.getValueSets().getDef()); } return valueSetDefs; - } + } - public static CqlTranslatorOptions getTranslatorOptions(String folder) { + public static CqlTranslatorOptions getTranslatorOptions(String folder) { String optionsFileName = folder + File.separator + "cql-options.json"; - CqlTranslatorOptions options = null; + CqlTranslatorOptions options; File file = new File(optionsFileName); if (file.exists()) { - options = CqlTranslatorOptionsMapper.fromFile(file.getAbsolutePath()); - logger.debug("cql-options loaded from: {}", file.getAbsolutePath()); + options = CqlTranslatorOptionsMapper.fromFile(file.getAbsolutePath()); + logger.debug("cql-options loaded from: {}", file.getAbsolutePath()); } else { - options = CqlTranslatorOptions.defaultOptions(); - if (!options.getFormats().contains(CqlTranslator.Format.XML)) { - options.getFormats().add(CqlTranslator.Format.XML); - } - logger.debug("cql-options not found. Using default options."); + options = CqlTranslatorOptions.defaultOptions(); + if (!options.getFormats().contains(CqlTranslator.Format.XML)) { + options.getFormats().add(CqlTranslator.Format.XML); + } + logger.debug("cql-options not found. Using default options."); } return options; - } + } - private static Map<String, org.hl7.elm.r1.Library> cachedElm = new HashMap<String, org.hl7.elm.r1.Library>(); - public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) { + private static final Map<String, org.hl7.elm.r1.Library> cachedElm = new HashMap<>(); + public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) { org.hl7.elm.r1.Library elm = cachedElm.get(cqlContentPath); if (elm != null) { - return elm; + return elm; } String folder = IOUtils.getParentDirectoryPath(cqlContentPath); @@ -413,7 +410,7 @@ public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) { ModelManager modelManager = new ModelManager(); LibraryManager libraryManager = new LibraryManager(modelManager); // if (packages != null) { - // libraryManager.getLibrarySourceLoader().registerProvider(new NpmLibrarySourceProvider(packages, reader, logger)); + // libraryManager.getLibrarySourceLoader().registerProvider(new NpmLibrarySourceProvider(packages, reader, logger)); // } libraryManager.getLibrarySourceLoader().registerProvider(new FhirLibrarySourceProvider()); libraryManager.getLibrarySourceLoader().registerProvider(new DefaultLibrarySourceProvider(Paths.get(folder))); @@ -435,412 +432,444 @@ public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) { elm = translator.toELM(); cachedElm.put(cqlContentPath, elm); return elm; - } + } - public static Boolean safeAddResource(String path, Map<String, IBaseResource> resources, FhirContext fhirContext) { - Boolean added = true; + public static Boolean safeAddResource(String path, Map<String, IBaseResource> resources, FhirContext fhirContext) { + boolean added = true; try { - IBaseResource resource = IOUtils.readResource(path, fhirContext, true); - if (resource != null) { + IBaseResource resource = IOUtils.readResource(path, fhirContext, true); + if (resource != null) { // if(resources.containsKey(resource.getIdElement().getIdPart())){ // IBaseResource storedResource = resources.get(resource.getIdElement().getIdPart()); // } - resources.putIfAbsent(resource.fhirType() + "/" + resource.getIdElement().getIdPart(), resource); - } else { + resources.putIfAbsent(resource.fhirType() + "/" + resource.getIdElement().getIdPart(), resource); + } else { added = false; LogUtils.putException(path, new Exception("Unable to add Resource: " + path)); - } + } } catch(Exception e) { - added = false; - LogUtils.putException(path, e); + added = false; + LogUtils.putException(path, e); } return added; - } + } - public static String getUrl(IBaseResource resource, FhirContext fhirContext) { + public static String getUrl(IBaseResource resource, FhirContext fhirContext) { switch (fhirContext.getVersion().getVersion()) { - case DSTU3: { - if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { - return ((org.hl7.fhir.dstu3.model.Measure)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.Library) { - return ((org.hl7.fhir.dstu3.model.Library)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { - return ((org.hl7.fhir.dstu3.model.PlanDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.CodeSystem) { - return ((org.hl7.fhir.dstu3.model.CodeSystem)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.ValueSet) { - return ((org.hl7.fhir.dstu3.model.ValueSet)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.ActivityDefinition) { - return ((org.hl7.fhir.dstu3.model.ActivityDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.StructureDefinition) { - return ((org.hl7.fhir.dstu3.model.StructureDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.GraphDefinition) { - return ((org.hl7.fhir.dstu3.model.GraphDefinition)resource).getUrl(); - } - throw new IllegalArgumentException(String.format("Could not retrieve url for resource type %s", resource.fhirType())); - } - case R4: { - if (resource instanceof org.hl7.fhir.r4.model.Measure) { - return ((org.hl7.fhir.r4.model.Measure)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.Library) { - return ((org.hl7.fhir.r4.model.Library)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { - return ((org.hl7.fhir.r4.model.PlanDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.CodeSystem) { - return ((org.hl7.fhir.r4.model.CodeSystem)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.ValueSet) { - return ((org.hl7.fhir.r4.model.ValueSet)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.ActivityDefinition) { - return ((org.hl7.fhir.r4.model.ActivityDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.StructureDefinition) { - return ((org.hl7.fhir.r4.model.StructureDefinition)resource).getUrl(); - } - if (resource instanceof org.hl7.fhir.r4.model.GraphDefinition) { - return ((org.hl7.fhir.r4.model.GraphDefinition)resource).getUrl(); - } - throw new IllegalArgumentException(String.format("Could not retrieve url for resource type %s", resource.fhirType())); - } - default: - throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); - } - } - - public static String getName(IBaseResource resource, FhirContext fhirContext) { - switch (fhirContext.getVersion().getVersion()) { - case DSTU3: { - if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { - return ((org.hl7.fhir.dstu3.model.Measure)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.Library) { - return ((org.hl7.fhir.dstu3.model.Library)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { - return ((org.hl7.fhir.dstu3.model.PlanDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.CodeSystem) { - return ((org.hl7.fhir.dstu3.model.CodeSystem)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.ValueSet) { - return ((org.hl7.fhir.dstu3.model.ValueSet)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.ActivityDefinition) { - return ((org.hl7.fhir.dstu3.model.ActivityDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.StructureDefinition) { - return ((org.hl7.fhir.dstu3.model.StructureDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.dstu3.model.GraphDefinition) { - return ((org.hl7.fhir.dstu3.model.GraphDefinition)resource).getName(); - } - throw new IllegalArgumentException(String.format("Could not retrieve name for resource type %s", resource.fhirType())); - } - case R4: { - if (resource instanceof org.hl7.fhir.r4.model.Measure) { - return ((org.hl7.fhir.r4.model.Measure)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.Library) { - return ((org.hl7.fhir.r4.model.Library)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { - return ((org.hl7.fhir.r4.model.PlanDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.CodeSystem) { - return ((org.hl7.fhir.r4.model.CodeSystem)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.ValueSet) { - return ((org.hl7.fhir.r4.model.ValueSet)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.ActivityDefinition) { - return ((org.hl7.fhir.r4.model.ActivityDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.StructureDefinition) { - return ((org.hl7.fhir.r4.model.StructureDefinition)resource).getName(); - } - if (resource instanceof org.hl7.fhir.r4.model.GraphDefinition) { - return ((org.hl7.fhir.r4.model.GraphDefinition)resource).getName(); - } - throw new IllegalArgumentException(String.format("Could not retrieve name for resource type %s", resource.fhirType())); + case DSTU3: { + if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { + return ((org.hl7.fhir.dstu3.model.Measure)resource).getUrl(); } - default: - throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); - } - } + if (resource instanceof org.hl7.fhir.dstu3.model.Library) { + return ((org.hl7.fhir.dstu3.model.Library)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { + return ((org.hl7.fhir.dstu3.model.PlanDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.CodeSystem) { + return ((org.hl7.fhir.dstu3.model.CodeSystem)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.ValueSet) { + return ((org.hl7.fhir.dstu3.model.ValueSet)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.ActivityDefinition) { + return ((org.hl7.fhir.dstu3.model.ActivityDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.StructureDefinition) { + return ((org.hl7.fhir.dstu3.model.StructureDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.GraphDefinition) { + return ((org.hl7.fhir.dstu3.model.GraphDefinition)resource).getUrl(); + } + throw new IllegalArgumentException(String.format("Could not retrieve url for resource type %s", resource.fhirType())); + } + case R4: { + if (resource instanceof org.hl7.fhir.r4.model.Measure) { + return ((org.hl7.fhir.r4.model.Measure)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.Library) { + return ((org.hl7.fhir.r4.model.Library)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { + return ((org.hl7.fhir.r4.model.PlanDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.CodeSystem) { + return ((org.hl7.fhir.r4.model.CodeSystem)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.ValueSet) { + return ((org.hl7.fhir.r4.model.ValueSet)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.ActivityDefinition) { + return ((org.hl7.fhir.r4.model.ActivityDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.StructureDefinition) { + return ((org.hl7.fhir.r4.model.StructureDefinition)resource).getUrl(); + } + if (resource instanceof org.hl7.fhir.r4.model.GraphDefinition) { + return ((org.hl7.fhir.r4.model.GraphDefinition)resource).getUrl(); + } + throw new IllegalArgumentException(String.format("Could not retrieve url for resource type %s", resource.fhirType())); + } + default: + throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static String getVersion(IBaseResource resource, FhirContext fhirContext) { + IBase version = TerserUtil.getValueFirstRep(fhirContext, resource, "version"); + if (version instanceof IPrimitiveType) { + return ((IPrimitiveType<?>) version).getValueAsString(); + } + else return null; + } + + public static String getName(IBaseResource resource, FhirContext fhirContext) { + switch (fhirContext.getVersion().getVersion()) { + case DSTU3: { + if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { + return ((org.hl7.fhir.dstu3.model.Measure)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.Library) { + return ((org.hl7.fhir.dstu3.model.Library)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { + return ((org.hl7.fhir.dstu3.model.PlanDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.CodeSystem) { + return ((org.hl7.fhir.dstu3.model.CodeSystem)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.ValueSet) { + return ((org.hl7.fhir.dstu3.model.ValueSet)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.ActivityDefinition) { + return ((org.hl7.fhir.dstu3.model.ActivityDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.StructureDefinition) { + return ((org.hl7.fhir.dstu3.model.StructureDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.dstu3.model.GraphDefinition) { + return ((org.hl7.fhir.dstu3.model.GraphDefinition)resource).getName(); + } + throw new IllegalArgumentException(String.format("Could not retrieve name for resource type %s", resource.fhirType())); + } + case R4: { + if (resource instanceof org.hl7.fhir.r4.model.Measure) { + return ((org.hl7.fhir.r4.model.Measure)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.Library) { + return ((org.hl7.fhir.r4.model.Library)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { + return ((org.hl7.fhir.r4.model.PlanDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.CodeSystem) { + return ((org.hl7.fhir.r4.model.CodeSystem)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.ValueSet) { + return ((org.hl7.fhir.r4.model.ValueSet)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.ActivityDefinition) { + return ((org.hl7.fhir.r4.model.ActivityDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.StructureDefinition) { + return ((org.hl7.fhir.r4.model.StructureDefinition)resource).getName(); + } + if (resource instanceof org.hl7.fhir.r4.model.GraphDefinition) { + return ((org.hl7.fhir.r4.model.GraphDefinition)resource).getName(); + } + throw new IllegalArgumentException(String.format("Could not retrieve name for resource type %s", resource.fhirType())); + } + default: + throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static String getPrimaryLibraryUrl(IBaseResource resource, FhirContext fhirContext) { + if (resource instanceof org.hl7.fhir.r5.model.Measure) { + org.hl7.fhir.r5.model.Measure measure = (org.hl7.fhir.r5.model.Measure)resource; + if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { + throw new IllegalArgumentException("Measure is expected to have one and only one library"); + } + return measure.getLibrary().get(0).getValue(); + } + else if (resource instanceof org.hl7.fhir.r4.model.Measure) { + org.hl7.fhir.r4.model.Measure measure = (org.hl7.fhir.r4.model.Measure)resource; + if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { + throw new IllegalArgumentException("Measure is expected to have one and only one library"); + } + return measure.getLibrary().get(0).getValue(); + } + else if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { + org.hl7.fhir.dstu3.model.Measure measure = (org.hl7.fhir.dstu3.model.Measure)resource; + if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { + throw new IllegalArgumentException("Measure is expected to have one and only one library"); + } + String reference = measure.getLibrary().get(0).getReference(); + String[] parts = reference.split("/"); + return parts[parts.length - 1]; + } + else if (resource instanceof org.hl7.fhir.r5.model.PlanDefinition) { + org.hl7.fhir.r5.model.PlanDefinition planDefinition = (org.hl7.fhir.r5.model.PlanDefinition)resource; + if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { + throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); + } + return planDefinition.getLibrary().get(0).getValue(); + } + else if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { + org.hl7.fhir.r4.model.PlanDefinition planDefinition = (org.hl7.fhir.r4.model.PlanDefinition)resource; + if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { + throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); + } + return planDefinition.getLibrary().get(0).getValue(); + } + else if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { + org.hl7.fhir.dstu3.model.PlanDefinition planDefinition = (org.hl7.fhir.dstu3.model.PlanDefinition)resource; + if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { + throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); + } + String reference = planDefinition.getLibrary().get(0).getReference(); + String[] parts = reference.split("/"); + return parts[parts.length - 1]; + } + else if (resource instanceof org.hl7.fhir.r5.model.Questionnaire) { + org.hl7.fhir.r5.model.Questionnaire questionnaire = (org.hl7.fhir.r5.model.Questionnaire)resource; + + org.hl7.fhir.r5.model.Extension libraryExtension = questionnaire.getExtensionByUrl(CQF_LIBRARY_EXT_URL); + if (libraryExtension != null) { + return ((org.hl7.fhir.r5.model.CanonicalType)libraryExtension.getValue()).getValueAsString(); + } + + return null; + } + else if (resource instanceof org.hl7.fhir.r4.model.Questionnaire) { + org.hl7.fhir.r4.model.Questionnaire questionnaire = (org.hl7.fhir.r4.model.Questionnaire)resource; + + org.hl7.fhir.r4.model.Extension libraryExtension = questionnaire.getExtensionByUrl(CQF_LIBRARY_EXT_URL); + if (libraryExtension != null) { + return ((CanonicalType)libraryExtension.getValue()).getValueAsString(); + } - public static String getPrimaryLibraryUrl(IBaseResource resource, FhirContext fhirContext) { - if (resource instanceof org.hl7.fhir.r5.model.Measure) { - org.hl7.fhir.r5.model.Measure measure = (org.hl7.fhir.r5.model.Measure)resource; + return null; + } + else if (resource instanceof org.hl7.fhir.dstu3.model.Questionnaire) { + org.hl7.fhir.dstu3.model.Questionnaire questionnaire = (org.hl7.fhir.dstu3.model.Questionnaire)resource; + + List<org.hl7.fhir.dstu3.model.Extension> libraryExtensions = + questionnaire.getExtensionsByUrl(CQF_LIBRARY_EXT_URL); + + if (libraryExtensions.isEmpty()) { + return null; + } else { + Validate.isTrue(libraryExtensions.size() == 1, "Url " + CQF_LIBRARY_EXT_URL + " must have only one match"); + return libraryExtensions.get(0).getValue().toString(); + } + } + else { + throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static String getPrimaryLibraryName(IBaseResource resource, FhirContext fhirContext) { + switch (fhirContext.getVersion().getVersion()) { + case DSTU3: { + org.hl7.fhir.dstu3.model.Measure measure = (org.hl7.fhir.dstu3.model.Measure)resource; if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { - throw new IllegalArgumentException("Measure is expected to have one and only one library"); + throw new IllegalArgumentException("Measure is expected to have one and only one library"); } - return measure.getLibrary().get(0).getValue(); - } - else if (resource instanceof org.hl7.fhir.r4.model.Measure) { + return getTail(measure.getLibrary().get(0).getReference()); + } + case R4: { org.hl7.fhir.r4.model.Measure measure = (org.hl7.fhir.r4.model.Measure)resource; if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { - throw new IllegalArgumentException("Measure is expected to have one and only one library"); + throw new IllegalArgumentException("Measure is expected to have one and only one library"); } - return measure.getLibrary().get(0).getValue(); - } - else if (resource instanceof org.hl7.fhir.dstu3.model.Measure) { - org.hl7.fhir.dstu3.model.Measure measure = (org.hl7.fhir.dstu3.model.Measure)resource; - if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { - throw new IllegalArgumentException("Measure is expected to have one and only one library"); - } - String reference = measure.getLibrary().get(0).getReference(); - String parts[] = reference.split("/"); - return parts[parts.length - 1]; - } - else if (resource instanceof org.hl7.fhir.r5.model.PlanDefinition) { - org.hl7.fhir.r5.model.PlanDefinition planDefinition = (org.hl7.fhir.r5.model.PlanDefinition)resource; - if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { - throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); - } - return planDefinition.getLibrary().get(0).getValue(); - } - else if (resource instanceof org.hl7.fhir.r4.model.PlanDefinition) { - org.hl7.fhir.r4.model.PlanDefinition planDefinition = (org.hl7.fhir.r4.model.PlanDefinition)resource; - if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { - throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); - } - return planDefinition.getLibrary().get(0).getValue(); - } - else if (resource instanceof org.hl7.fhir.dstu3.model.PlanDefinition) { - org.hl7.fhir.dstu3.model.PlanDefinition planDefinition = (org.hl7.fhir.dstu3.model.PlanDefinition)resource; - if (!planDefinition.hasLibrary() || planDefinition.getLibrary().size() != 1) { - throw new IllegalArgumentException("PlanDefinition is expected to have one and only one library"); - } - String reference = planDefinition.getLibrary().get(0).getReference(); - String parts[] = reference.split("/"); - return parts[parts.length - 1]; - } - else if (resource instanceof org.hl7.fhir.r5.model.Questionnaire) { - org.hl7.fhir.r5.model.Questionnaire questionnaire = (org.hl7.fhir.r5.model.Questionnaire)resource; - - org.hl7.fhir.r5.model.Extension libraryExtension = questionnaire.getExtensionByUrl(cqfLibraryExtensionUrl); - if (libraryExtension != null) { - return ((org.hl7.fhir.r5.model.CanonicalType)libraryExtension.getValue()).getValueAsString(); + return getTail(measure.getLibrary().get(0).getValue()); + } + default: + throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static VersionedIdentifier getIdentifier(IBaseResource resource, FhirContext fhirContext) { + String url = getUrl(resource, fhirContext); + return CanonicalUtils.toVersionedIdentifierAnyResource(url).withVersion(getVersion(resource, fhirContext)); + } + + public static boolean compareResourcePrimitiveElements(IBaseResource res1, IBaseResource res2, + FhirContext fhirContext, String... elements) { + AtomicBoolean match = new AtomicBoolean(true); + if (res1 != null && res2 != null && res1.fhirType().equals(res2.fhirType())) { + Arrays.stream(elements).forEach(element -> { + IBase e1 = TerserUtil.getValueFirstRep(fhirContext, res1, element); + IBase e2 = TerserUtil.getValueFirstRep(fhirContext, res2, element); + if (e1 instanceof IPrimitiveType && e2 instanceof IPrimitiveType + && !((IPrimitiveType<?>) e1).getValueAsString().equals(((IPrimitiveType<?>) e2).getValueAsString())) { + match.set(false); } + }); + } + return match.get(); + } - return null; - } - else if (resource instanceof org.hl7.fhir.r4.model.Questionnaire) { - org.hl7.fhir.r4.model.Questionnaire questionnaire = (org.hl7.fhir.r4.model.Questionnaire)resource; + public static boolean compareResourceIdUrlAndVersion(IBaseResource res1, IBaseResource res2, + FhirContext fhirContext) { + return compareResourcePrimitiveElements(res1, res2, fhirContext, "id", "url", "version"); + } - org.hl7.fhir.r4.model.Extension libraryExtension = questionnaire.getExtensionByUrl(cqfLibraryExtensionUrl); - if (libraryExtension != null) { - return ((CanonicalType)libraryExtension.getValue()).getValueAsString(); - } + public static Map<String, IBaseResource> getActivityDefinitionResources(String planDefinitionPath, FhirContext fhirContext, Boolean includeVersion) { + Map<String, IBaseResource> activityDefinitions = new HashMap<>(); + IBaseResource planDefinition = IOUtils.readResource(planDefinitionPath, fhirContext, true); + Object actionChild = resolveProperty(planDefinition, "action", fhirContext); - return null; - } - else if (resource instanceof org.hl7.fhir.dstu3.model.Questionnaire) { - org.hl7.fhir.dstu3.model.Questionnaire questionnaire = (org.hl7.fhir.dstu3.model.Questionnaire)resource; - - List<org.hl7.fhir.dstu3.model.Extension> libraryExtensions = - questionnaire.getExtensionsByUrl(cqfLibraryExtensionUrl); - - if (libraryExtensions.size() == 0) { - return null; - } else { - Validate.isTrue(libraryExtensions.size() == 1, "Url " + cqfLibraryExtensionUrl + " must have only one match", new Object[0]); - return libraryExtensions.get(0).getValue().toString(); - } - } - else { - throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); - } - } - - public static String getPrimaryLibraryName(IBaseResource resource, FhirContext fhirContext) { - switch (fhirContext.getVersion().getVersion()) { - case DSTU3: { - org.hl7.fhir.dstu3.model.Measure measure = (org.hl7.fhir.dstu3.model.Measure)resource; - if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { - throw new IllegalArgumentException("Measure is expected to have one and only one library"); - } - return getTail(measure.getLibrary().get(0).getReference()); - } - case R4: { - org.hl7.fhir.r4.model.Measure measure = (org.hl7.fhir.r4.model.Measure)resource; - if (!measure.hasLibrary() || measure.getLibrary().size() != 1) { - throw new IllegalArgumentException("Measure is expected to have one and only one library"); - } - return getTail(measure.getLibrary().get(0).getValue()); - } - default: - throw new IllegalArgumentException("Unsupported fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); - } - } - - public static Map<String, IBaseResource> getActivityDefinitionResources(String planDefinitionPath, FhirContext fhirContext, Boolean includeVersion) { - Map<String, IBaseResource> activityDefinitions = new HashMap<String, IBaseResource>(); - IBaseResource planDefinition = IOUtils.readResource(planDefinitionPath, fhirContext, true); - Object actionChild = resolveProperty(planDefinition, "action", fhirContext); - - if (actionChild != null) { - if (actionChild instanceof Iterable) - { + if (actionChild != null) { + if (actionChild instanceof Iterable) + { for (Object action : (Iterable<?>)actionChild) { - Object definitionChild = resolveProperty(action, "definition", fhirContext); - if (definitionChild != null) { - Object referenceChild = resolveProperty(definitionChild, "reference", fhirContext); - - String activityDefinitionId = null; - // NOTE: A bit of a hack. This whole method probably needs to be refactored to consider different FHIR - // versions and the respective ActivityDefinition differences between them. - if (fhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.R4)) { - activityDefinitionId = CanonicalUtils.getId((CanonicalType)referenceChild); - } else { - String activityDefinitionReference = referenceChild.toString(); - activityDefinitionId = activityDefinitionReference.replaceAll("ActivityDefinition/", "activitydefinition-").replaceAll("_", "-"); - } - - for (String path : IOUtils.getActivityDefinitionPaths(fhirContext)) { - if (path.contains(activityDefinitionId)) { - activityDefinitions.put(path, IOUtils.readResource(path, fhirContext)); - break; + Object definitionChild = resolveProperty(action, "definition", fhirContext); + if (definitionChild != null) { + Object referenceChild = resolveProperty(definitionChild, "reference", fhirContext); + + String activityDefinitionId = null; + // NOTE: A bit of a hack. This whole method probably needs to be refactored to consider different FHIR + // versions and the respective ActivityDefinition differences between them. + if (fhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.R4)) { + activityDefinitionId = CanonicalUtils.getId((CanonicalType)referenceChild); + } else { + String activityDefinitionReference = referenceChild.toString(); + activityDefinitionId = activityDefinitionReference.replace("ActivityDefinition/", "activitydefinition-").replace("_", "-"); + } + + for (String path : IOUtils.getActivityDefinitionPaths(fhirContext)) { + if (path.contains(activityDefinitionId)) { + activityDefinitions.put(path, IOUtils.readResource(path, fhirContext)); + break; + } } - } - } + } } - } - else { + } + else { Object definitionChild = resolveProperty(actionChild, "definition", fhirContext); if (definitionChild != null) { - Object referenceChild = resolveProperty(definitionChild, "reference", fhirContext); + Object referenceChild = resolveProperty(definitionChild, "reference", fhirContext); - String activityDefinitionReference = (String)referenceChild; + String activityDefinitionReference = (String)referenceChild; - for (String path : IOUtils.getActivityDefinitionPaths(fhirContext)) { - if (path.contains(activityDefinitionReference)) { - activityDefinitions.put(path, IOUtils.readResource(path, fhirContext)); - } - } + for (String path : IOUtils.getActivityDefinitionPaths(fhirContext)) { + if (path.contains(activityDefinitionReference)) { + activityDefinitions.put(path, IOUtils.readResource(path, fhirContext)); + } + } } - } - } - return activityDefinitions; - } + } + } + return activityDefinitions; + } - public static Object resolveProperty(Object target, String path, FhirContext fhirContext) { + public static Object resolveProperty(Object target, String path, FhirContext fhirContext) { if (target == null) { - return null; + return null; } IBase base = (IBase) target; if (base instanceof IPrimitiveType) { - return path.equals("value") ? ((IPrimitiveType<?>) target).getValue() : target; + return path.equals("value") ? ((IPrimitiveType<?>) target).getValue() : target; } BaseRuntimeElementCompositeDefinition<?> definition = resolveRuntimeDefinition(base, fhirContext); BaseRuntimeChildDefinition child = definition.getChildByName(path); if (child == null) { - child = resolveChoiceProperty(definition, path); + child = resolveChoiceProperty(definition, path); } List<IBase> values = child.getAccessor().getValues(base); if (values == null || values.isEmpty()) { - return null; + return null; } if (child instanceof RuntimeChildChoiceDefinition && !child.getElementName().equalsIgnoreCase(path)) { - if (!values.get(0).getClass().getSimpleName().equalsIgnoreCase(child.getChildByName(path).getImplementingClass().getSimpleName())) - { - return null; - } + if (!values.get(0).getClass().getSimpleName().equalsIgnoreCase(child.getChildByName(path).getImplementingClass().getSimpleName())) + { + return null; + } } //Hack to get DecimalType to work if (child.getMax() == 1 && values.get(0) instanceof org.hl7.fhir.dstu3.model.DecimalType) { - return resolveProperty(values.get(0), "value", fhirContext); + return resolveProperty(values.get(0), "value", fhirContext); } if (child.getMax() == 1 && values.get(0) instanceof org.hl7.fhir.r4.model.DecimalType) { - return resolveProperty(values.get(0), "value", fhirContext); + return resolveProperty(values.get(0), "value", fhirContext); } return child.getMax() < 1 ? values : values.get(0); - } + } - public static BaseRuntimeElementCompositeDefinition<?> resolveRuntimeDefinition(IBase base, FhirContext fhirContext) { + public static BaseRuntimeElementCompositeDefinition<?> resolveRuntimeDefinition(IBase base, FhirContext fhirContext) { if (base instanceof IBaseResource) { - return fhirContext.getResourceDefinition((IBaseResource) base); + return fhirContext.getResourceDefinition((IBaseResource) base); } else if (base instanceof IBaseBackboneElement || base instanceof IBaseElement) { - return (BaseRuntimeElementCompositeDefinition<?>) fhirContext.getElementDefinition(base.getClass()); + return (BaseRuntimeElementCompositeDefinition<?>) fhirContext.getElementDefinition(base.getClass()); } else if (base instanceof ICompositeType) { - return (RuntimeCompositeDatatypeDefinition) fhirContext.getElementDefinition(base.getClass()); + return (RuntimeCompositeDatatypeDefinition) fhirContext.getElementDefinition(base.getClass()); } //should be UnkownType throw new Error(String.format("Unable to resolve the runtime definition for %s", base.getClass().getName())); - } + } - public static BaseRuntimeChildDefinition resolveChoiceProperty(BaseRuntimeElementCompositeDefinition<?> definition, String path) { - for (Object child : definition.getChildren()) { - if (child instanceof RuntimeChildChoiceDefinition) { - RuntimeChildChoiceDefinition choiceDefinition = (RuntimeChildChoiceDefinition) child; + public static BaseRuntimeChildDefinition resolveChoiceProperty(BaseRuntimeElementCompositeDefinition<?> definition, String path) { + for (var child : definition.getChildren()) { + if (child instanceof RuntimeChildChoiceDefinition) { + RuntimeChildChoiceDefinition choiceDefinition = (RuntimeChildChoiceDefinition) child; - if (choiceDefinition.getElementName().startsWith(path)) { - return choiceDefinition; - } - } + if (choiceDefinition.getElementName().startsWith(path)) { + return choiceDefinition; + } + } } //UnkownType throw new Error(String.format("Unable to resolve path %s for %s", path, definition.getName())); - } - - public static RuntimeResourceDefinition getResourceDefinition(FhirContext fhirContext, String ResourceName) { - RuntimeResourceDefinition def = fhirContext.getResourceDefinition(ResourceName); - return def; - } - - public static BaseRuntimeElementDefinition<?> getElementDefinition(FhirContext fhirContext, String ElementName) { - BaseRuntimeElementDefinition<?> def = fhirContext.getElementDefinition(ElementName); - return def; - } - - public static void outputResource(IBaseResource resource, String encoding, FhirContext context, String outputPath) { - try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + "." + encoding)) { - writer.write( - encoding.equals("json") - ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - ); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e.getMessage()); - } - } - - public static void outputResourceByName(IBaseResource resource, String encoding, FhirContext context, String outputPath, String name) { - try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) { - writer.write( - encoding.equals("json") - ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - ); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e.getMessage()); - } - } -} + } + + public static RuntimeResourceDefinition getResourceDefinition(FhirContext fhirContext, String resourceName) { + return fhirContext.getResourceDefinition(resourceName); + } + + public static BaseRuntimeElementDefinition<?> getElementDefinition(FhirContext fhirContext, String elementName) { + return fhirContext.getElementDefinition(elementName); + } + + public static void outputResource(IBaseResource resource, String encoding, FhirContext context, String outputPath) { + try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + "." + encoding)) { + writer.write( + encoding.equals("json") + ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + ); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } + + public static void outputResourceByName(IBaseResource resource, String encoding, FhirContext context, String outputPath, String name) { + try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) { + writer.write( + encoding.equals("json") + ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + ); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/STU3FHIRUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/STU3FHIRUtils.java index 588840673..7f7dedf46 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/STU3FHIRUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/STU3FHIRUtils.java @@ -21,6 +21,8 @@ public class STU3FHIRUtils { + private STU3FHIRUtils() {} + public static Coding toCoding(Code code, CompiledLibrary library, LibraryManager libraryManager) { CodeSystemDef codeSystemDef = resolveCodeSystemRef(code.getSystem(), library, libraryManager); Coding coding = new Coding(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/FhirVersionEnumConverter.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/FhirVersionEnumConverter.java index f2c22e7f1..2dc710eb6 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/FhirVersionEnumConverter.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/FhirVersionEnumConverter.java @@ -14,6 +14,7 @@ public FhirVersionEnum convert(String value) { switch(value.trim().toUpperCase()) { case "DSTU3": return FhirVersionEnum.DSTU3; case "R4": return FhirVersionEnum.R4; + case "R5": return FhirVersionEnum.R5; default: throw new IllegalArgumentException(String.format("unknown or unsupported FHIR version %s", value)); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/ResourceAndTypeConverter.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/ResourceAndTypeConverter.java new file mode 100644 index 000000000..9bb6f355d --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/converters/ResourceAndTypeConverter.java @@ -0,0 +1,111 @@ +package org.opencds.cqf.tooling.utilities.converters; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_50; +import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; +import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50; +import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class ResourceAndTypeConverter { + private static final VersionConvertor_30_50 stu3ToR5Converter = new VersionConvertor_30_50(new BaseAdvisor_30_50()); + private static final VersionConvertor_40_50 r4ToR5Converter = new VersionConvertor_40_50(new BaseAdvisor_40_50()); + + private ResourceAndTypeConverter() { + + } + + public static IBaseDatatype convertType(FhirContext fhirContext, IBaseDatatype type) { + switch (fhirContext.getVersion().getVersion()) { + case R5: + if (type instanceof org.hl7.fhir.dstu3.model.Type) { + return stu3ToR5Type((org.hl7.fhir.dstu3.model.Type) type); + } + else if (type instanceof org.hl7.fhir.r4.model.Type) { + return r4ToR5Type((org.hl7.fhir.r4.model.Type) type); + } + else { + throw new UnsupportedOperationException( + "Conversion to R5 type not supported for " + type.getClass().toString()); + } + case R4: + if (type instanceof org.hl7.fhir.r5.model.DataType) { + return r5ToR4Type((org.hl7.fhir.r5.model.DataType) type); + } + else { + throw new UnsupportedOperationException( + "Conversion to R4 type not supported for " + type.getClass().toString()); + } + case DSTU3: + if (type instanceof org.hl7.fhir.r5.model.DataType) { + return r5ToStu3Type((org.hl7.fhir.r5.model.DataType) type); + } + else { + throw new UnsupportedOperationException( + "Conversion to DSTU3 type not supported for " + type.getClass().toString()); + } + default: + throw new UnsupportedOperationException( + "Conversion not supported for types using version " + + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static org.hl7.fhir.r5.model.Resource convertToR5Resource(FhirContext fhirContext, IBaseResource resource) { + switch (fhirContext.getVersion().getVersion()) { + case R5: return (org.hl7.fhir.r5.model.Resource) resource; + case R4: return r4ToR5Resource(resource); + case DSTU3: return stu3ToR5Resource(resource); + default: + throw new UnsupportedOperationException( + "Conversion to R5 not supported for resources using version " + + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static IBaseResource convertFromR5Resource(FhirContext fhirContext, org.hl7.fhir.r5.model.Resource resource) { + switch (fhirContext.getVersion().getVersion()) { + case R5: return resource; + case R4: return r5ToR4Resource(resource); + case DSTU3: return r5ToStu3Resource(resource); + default: + throw new UnsupportedOperationException( + "Conversion from R5 not supported for resources using version " + + fhirContext.getVersion().getVersion().getFhirVersionString()); + } + } + + public static org.hl7.fhir.r5.model.Resource stu3ToR5Resource(IBaseResource resourceToConvert) { + return stu3ToR5Converter.convertResource((org.hl7.fhir.dstu3.model.Resource) resourceToConvert); + } + + public static org.hl7.fhir.dstu3.model.Resource r5ToStu3Resource(IBaseResource resourceToConvert) { + return stu3ToR5Converter.convertResource((org.hl7.fhir.r5.model.Resource) resourceToConvert); + } + + public static org.hl7.fhir.r5.model.DataType stu3ToR5Type(org.hl7.fhir.dstu3.model.Type typeToConvert) { + return stu3ToR5Converter.convertType(typeToConvert); + } + + public static org.hl7.fhir.dstu3.model.Type r5ToStu3Type(org.hl7.fhir.r5.model.DataType typeToConvert) { + return stu3ToR5Converter.convertType(typeToConvert); + } + + public static org.hl7.fhir.r5.model.Resource r4ToR5Resource(IBaseResource resourceToConvert) { + return r4ToR5Converter.convertResource((org.hl7.fhir.r4.model.Resource) resourceToConvert); + } + + public static org.hl7.fhir.r4.model.Resource r5ToR4Resource(IBaseResource resourceToConvert) { + return r4ToR5Converter.convertResource((org.hl7.fhir.r5.model.Resource) resourceToConvert); + } + + public static org.hl7.fhir.r5.model.DataType r4ToR5Type(org.hl7.fhir.r4.model.Type typeToConvert) { + return r4ToR5Converter.convertType(typeToConvert); + } + + public static org.hl7.fhir.r4.model.Type r5ToR4Type(org.hl7.fhir.r5.model.DataType typeToConvert) { + return r4ToR5Converter.convertType(typeToConvert); + } + +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/bundle/BundleOperationsIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/bundle/BundleOperationsIT.java new file mode 100644 index 000000000..746cc6258 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/bundle/BundleOperationsIT.java @@ -0,0 +1,66 @@ +package org.opencds.cqf.tooling.operations.bundle; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Patient; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class BundleOperationsIT { + + + + @Test + void TestBundleOperations() { + FhirContext context = FhirContext.forR4Cached(); + String bundleId = "test-collection-bundle"; + BundleTypeEnum type = BundleTypeEnum.COLLECTION; + List<IBaseResource> resourcesToBundle = Arrays.asList( + new Patient().setId("test-1"), + new Patient().setId("test-2") + ); + + IBaseBundle bundle = BundleResources.bundleResources(context, bundleId, type, resourcesToBundle); + assertTrue(bundle instanceof Bundle); + assertEquals(((Bundle) bundle).getId(), bundleId); + assertEquals(((Bundle) bundle).getType().toCode(), type.getCode()); + assertEquals(((Bundle) bundle).getEntry().size(), 2); + assertTrue(((Bundle) bundle).getEntryFirstRep().hasResource()); + assertTrue(((Bundle) bundle).getEntryFirstRep().getResource() instanceof Patient); + + List<IBaseResource> resourcesFromBundle = BundleToResources.bundleToResources(context, bundle); + assertEquals(resourcesFromBundle.size(), 2); + assertTrue(resourcesFromBundle.get(0) instanceof Patient); + + bundleId = "test-transaction-bundle"; + bundle = BundleResources.bundleResources(context, bundleId, null, resourcesToBundle); + assertTrue(bundle instanceof Bundle); + assertEquals(bundleId, ((Bundle) bundle).getId()); + assertEquals(((Bundle) bundle).getType().toCode(), "transaction"); + assertEquals(((Bundle) bundle).getEntry().size(), 2); + assertTrue(((Bundle) bundle).getEntryFirstRep().hasRequest()); + assertTrue(((Bundle) bundle).getEntryFirstRep().getRequest().hasMethod()); + assertEquals(((Bundle) bundle).getEntryFirstRep().getRequest().getMethod(), Bundle.HTTPVerb.PUT); + assertTrue(((Bundle) bundle).getEntryFirstRep().hasResource()); + assertTrue(((Bundle) bundle).getEntryFirstRep().getResource() instanceof Patient); + + bundle = BundleResources.bundleResources(context, null, null, Collections.emptyList()); + assertTrue(bundle instanceof Bundle); + assertTrue(((Bundle) bundle).hasId()); + assertEquals(((Bundle) bundle).getType().toCode(), "transaction"); + assertFalse(((Bundle) bundle).hasEntry()); + + resourcesFromBundle = BundleToResources.bundleToResources(context, bundle); + assertTrue(resourcesFromBundle.isEmpty()); + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/LoincHierarchyProcessorIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/LoincHierarchyProcessorIT.java new file mode 100644 index 000000000..acf64a319 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/LoincHierarchyProcessorIT.java @@ -0,0 +1,2024 @@ +package org.opencds.cqf.tooling.operations.codesystem; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ValueSet; +import org.mockserver.integration.ClientAndServer; +import org.opencds.cqf.tooling.constants.Terminology; +import org.opencds.cqf.tooling.operations.codesystem.loinc.HierarchyProcessor; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +public class LoincHierarchyProcessorIT { + + // Simulates the LOINC hierarchy API at https://loinc.regenstrief.org/searchapi/hierarchy/component-system/search?searchString= + // Cannot test that endpoint due to authorization requirements + private ClientAndServer testClient; + + @BeforeClass + void setup() { + testClient = ClientAndServer.startClientAndServer(1080); + + // simple query response + testClient.when( + request() + .withMethod("GET") + .withQueryStringParameter("queryString", "cocaine (=system:Urine) AND NOT status:DEPRECATED") + ).respond( + response().withBody(COCAINE_URINE_QUERY_RESPONSE) + ); + + // simple multiaxial_descendantsof query + testClient.when( + request() + .withMethod("GET") + .withQueryStringParameter("queryString", "multiaxial_descendantsof:LP14348-4 AND SYSTEM:Urine") + ).respond( + response().withBody(CANNABINOID_QUERY_RESPONSE) + ); + } + + @AfterClass + void teardown() { + testClient.stop(); + } + + @Test + void testSimpleQuery() { + HierarchyProcessor hierarchyProcessor = new HierarchyProcessor(); + hierarchyProcessor.setFhirContext(FhirContext.forR4Cached()); + hierarchyProcessor.setQuery("cocaine (=system:Urine) AND NOT status:DEPRECATED"); + hierarchyProcessor.setLoincHierarchyUrl("http://localhost:1080?queryString="); + + IBaseResource returnVs = hierarchyProcessor.getValueSet(); + Assert.assertTrue(returnVs instanceof ValueSet); + Assert.assertTrue(((ValueSet) returnVs).hasCompose()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().hasInclude()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().getIncludeFirstRep().hasSystem()); + Assert.assertEquals(((ValueSet) returnVs).getCompose().getIncludeFirstRep().getSystem(), Terminology.LOINC_SYSTEM_URL); + Assert.assertEquals(((ValueSet) returnVs).getCompose().getIncludeFirstRep().getConcept().size(), 41); + } + + @Test + void testSimpleDescendantsOfQuery() { + HierarchyProcessor hierarchyProcessor = new HierarchyProcessor(); + hierarchyProcessor.setFhirContext(FhirContext.forR4Cached()); + hierarchyProcessor.setQuery("multiaxial_descendantsof:LP14348-4 AND SYSTEM:Urine"); + hierarchyProcessor.setLoincHierarchyUrl("http://localhost:1080?queryString="); + + IBaseResource returnVs = hierarchyProcessor.getValueSet(); + Assert.assertTrue(returnVs instanceof ValueSet); + Assert.assertTrue(((ValueSet) returnVs).hasCompose()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().hasInclude()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().getIncludeFirstRep().hasSystem()); + Assert.assertEquals(((ValueSet) returnVs).getCompose().getIncludeFirstRep().getSystem(), Terminology.LOINC_SYSTEM_URL); + Assert.assertEquals(((ValueSet) returnVs).getCompose().getIncludeFirstRep().getConcept().size(), 19); + } + + private final String COCAINE_URINE_QUERY_RESPONSE = "[\n" + + " {\n" + + " \"Id\": -70212289,\n" + + " \"Code\": \"LP432695-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP432695-7\",\n" + + " \"CodeText\": \"{component}\",\n" + + " \"Level\": 0,\n" + + " \"LoincAncestorCount\": 102996,\n" + + " \"PathEnumeration\": \"00001\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14010013,\n" + + " \"ParentId\": -70212289,\n" + + " \"Code\": \"LP29693-6\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP29693-6\",\n" + + " \"CodeText\": \"Laboratory\",\n" + + " \"Level\": 1,\n" + + " \"LoincAncestorCount\": 62659,\n" + + " \"PathEnumeration\": \"00001.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -26057072,\n" + + " \"ParentId\": -14010013,\n" + + " \"Code\": \"LP7790-1\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP7790-1\",\n" + + " \"CodeText\": \"Drug toxicology\",\n" + + " \"Level\": 2,\n" + + " \"LoincAncestorCount\": 8636,\n" + + " \"PathEnumeration\": \"00001.00001.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054262,\n" + + " \"ParentId\": -26057072,\n" + + " \"Code\": \"LP18046-0\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP18046-0\",\n" + + " \"CodeText\": \"Drugs\",\n" + + " \"Level\": 3,\n" + + " \"LoincAncestorCount\": 6367,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054410,\n" + + " \"ParentId\": -14054262,\n" + + " \"Code\": \"LP31450-7\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP31450-7\",\n" + + " \"CodeText\": \"Anti-infection drugs\",\n" + + " \"Level\": 4,\n" + + " \"LoincAncestorCount\": 463,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00008\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054411,\n" + + " \"ParentId\": -14054410,\n" + + " \"Code\": \"LP31426-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP31426-7\",\n" + + " \"CodeText\": \"Antibiotics\",\n" + + " \"Level\": 5,\n" + + " \"LoincAncestorCount\": 298,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00008.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054478,\n" + + " \"ParentId\": -14054411,\n" + + " \"Code\": \"LP100040-7\",\n" + + " \"Sequence\": 54,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP100040-7\",\n" + + " \"CodeText\": \"Levamisole\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 5,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00008.00001.00054\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89630682,\n" + + " \"ParentId\": -14054478,\n" + + " \"Code\": \"LP388980-7\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP388980-7\",\n" + + " \"CodeText\": \"Levamisole | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00008.00001.00054.00005\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179828967,\n" + + " \"ParentId\": 89630682,\n" + + " \"Code\": \"59295-6\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"59295-6\",\n" + + " \"CodeText\": \"Levamisole Ur-mCnc\",\n" + + " \"Component\": \"Levamisole\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00008.00001.00054.00005.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054713,\n" + + " \"ParentId\": -14054262,\n" + + " \"Code\": \"LP31448-1\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP31448-1\",\n" + + " \"CodeText\": \"Controlled substances and drugs of abuse\",\n" + + " \"Level\": 4,\n" + + " \"LoincAncestorCount\": 1786,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054797,\n" + + " \"ParentId\": -14054713,\n" + + " \"Code\": \"LP16048-8\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP16048-8\",\n" + + " \"CodeText\": \"Cocaine\",\n" + + " \"Level\": 5,\n" + + " \"LoincAncestorCount\": 149,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054798,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP16047-0\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP16047-0\",\n" + + " \"CodeText\": \"Benzoylecgonine\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 48,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631428,\n" + + " \"ParentId\": -14054798,\n" + + " \"Code\": \"LP389701-6\",\n" + + " \"Sequence\": 11,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389701-6\",\n" + + " \"CodeText\": \"Benzoylecgonine | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 16,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179579632,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"3394-4\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"3394-4\",\n" + + " \"CodeText\": \"BZE Ur-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179415838,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"16226-3\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"16226-3\",\n" + + " \"CodeText\": \"BZE Ur Cfm-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179912178,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"70146-6\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"70146-6\",\n" + + " \"CodeText\": \"BZE Ur Scn-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00005\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179579559,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"3393-6\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"3393-6\",\n" + + " \"CodeText\": \"BZE Ur Ql\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179397166,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"14315-6\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"14315-6\",\n" + + " \"CodeText\": \"BZE Ur Ql Cfm\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180017879,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"8192-7\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"8192-7\",\n" + + " \"CodeText\": \"BZE Ur Ql SAMHSA Cfm\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"SAMHSA confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180017932,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"8193-5\",\n" + + " \"Sequence\": 9,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"8193-5\",\n" + + " \"CodeText\": \"BZE Ur Ql SAMHSA Scn\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"SAMHSA screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00009\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179397162,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"14314-9\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"14314-9\",\n" + + " \"CodeText\": \"BZE Ur Ql Scn\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00010\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179690474,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"43984-4\",\n" + + " \"Sequence\": 11,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"43984-4\",\n" + + " \"CodeText\": \"BZE Ur Ql Scn>150 ng/mL\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen>150 ng/mL\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00011\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179690477,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"43985-1\",\n" + + " \"Sequence\": 12,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"43985-1\",\n" + + " \"CodeText\": \"BZE Ur Ql Scn>300 ng/mL\",\n" + + " \"Component\": \"Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen>300 ng/mL\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00012\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179440718,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"19065-2\",\n" + + " \"Sequence\": 13,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19065-2\",\n" + + " \"CodeText\": \"BZE CtO Ur-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00013\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442834,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"19358-1\",\n" + + " \"Sequence\": 14,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19358-1\",\n" + + " \"CodeText\": \"BZE CtO Ur Cfm-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00014\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442831,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"19357-3\",\n" + + " \"Sequence\": 15,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19357-3\",\n" + + " \"CodeText\": \"BZE CtO Ur Scn-mCnc\",\n" + + " \"Component\": \"Benzoylecgonine cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00015\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179389716,\n" + + " \"ParentId\": 89631428,\n" + + " \"Code\": \"13479-1\",\n" + + " \"Sequence\": 16,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"13479-1\",\n" + + " \"CodeText\": \"BZE/Creat Ur\",\n" + + " \"Component\": \"Benzoylecgonine/Creatinine\",\n" + + " \"Property\": \"MRto\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00001.00011.00016\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054799,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP36028-6\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP36028-6\",\n" + + " \"CodeText\": \"Cocaine+Benzoylecgonine\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 7,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631439,\n" + + " \"ParentId\": -14054799,\n" + + " \"Code\": \"LP389712-3\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389712-3\",\n" + + " \"CodeText\": \"Cocaine+Benzoylecgonine | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 2,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00004.00006\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179720326,\n" + + " \"ParentId\": 89631439,\n" + + " \"Code\": \"47400-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"47400-7\",\n" + + " \"CodeText\": \"Cocaine+BZE Ur Ql Cfm\",\n" + + " \"Component\": \"Cocaine+Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00004.00006.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179673449,\n" + + " \"ParentId\": 89631439,\n" + + " \"Code\": \"42241-0\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"42241-0\",\n" + + " \"CodeText\": \"Cocaine+BZE Ur Ql Scn\",\n" + + " \"Component\": \"Cocaine+Benzoylecgonine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00004.00006.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054800,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP230504-5\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP230504-5\",\n" + + " \"CodeText\": \"Cocaine+Benzoylecgonine+Cocaethylene\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631440,\n" + + " \"ParentId\": -14054800,\n" + + " \"Code\": \"LP389713-1\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389713-1\",\n" + + " \"CodeText\": \"Cocaine+Benzoylecgonine+Cocaethylene | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00006.00001\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 180025622,\n" + + " \"ParentId\": 89631440,\n" + + " \"Code\": \"82723-8\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"82723-8\",\n" + + " \"CodeText\": \"Coc+Benzoylecgon+Cocaethyl Ur Ql Cfm\",\n" + + " \"Component\": \"Cocaine+Benzoylecgonine+Cocaethylene\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00006.00001.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054801,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP18505-5\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP18505-5\",\n" + + " \"CodeText\": \"Cocaethylene\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 23,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631448,\n" + + " \"ParentId\": -14054801,\n" + + " \"Code\": \"LP389720-6\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389720-6\",\n" + + " \"CodeText\": \"Cocaethylene | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 7,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179443204,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"19408-4\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19408-4\",\n" + + " \"CodeText\": \"Cocaethylene Ur-mCnc\",\n" + + " \"Component\": \"Cocaethylene\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179419560,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"16632-2\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"16632-2\",\n" + + " \"CodeText\": \"Cocaethylene Ur Cfm-mCnc\",\n" + + " \"Component\": \"Cocaethylene\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179443201,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"19406-8\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19406-8\",\n" + + " \"CodeText\": \"Cocaethylene Ur Ql Cfm\",\n" + + " \"Component\": \"Cocaethylene\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179443186,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"19405-0\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19405-0\",\n" + + " \"CodeText\": \"Cocaethylene Ur Ql Scn\",\n" + + " \"Component\": \"Cocaethylene\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179443235,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"19410-0\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19410-0\",\n" + + " \"CodeText\": \"Cocaethylene CtO Ur Cfm-mCnc\",\n" + + " \"Component\": \"Cocaethylene cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00005\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179443214,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"19409-2\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19409-2\",\n" + + " \"CodeText\": \"Cocaethylene CtO Ur Scn-mCnc\",\n" + + " \"Component\": \"Cocaethylene cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180062773,\n" + + " \"ParentId\": 89631448,\n" + + " \"Code\": \"86606-1\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"86606-1\",\n" + + " \"CodeText\": \"Cocaethylene/Creat Ur Cfm\",\n" + + " \"Component\": \"Cocaethylene/Creatinine\",\n" + + " \"Property\": \"MRto\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00007.00010.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054802,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP35633-4\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP35633-4\",\n" + + " \"CodeText\": \"3-Hydroxybenzoylecgonine\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 4,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00010\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631455,\n" + + " \"ParentId\": -14054802,\n" + + " \"Code\": \"LP389727-1\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389727-1\",\n" + + " \"CodeText\": \"3-Hydroxybenzoylecgonine | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00010.00003\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179751891,\n" + + " \"ParentId\": 89631455,\n" + + " \"Code\": \"50594-1\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"50594-1\",\n" + + " \"CodeText\": \"3OH-BZE Ur Cfm-mCnc\",\n" + + " \"Component\": \"3-Hydroxybenzoylecgonine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00010.00003.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054803,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP28530-1\",\n" + + " \"Sequence\": 11,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP28530-1\",\n" + + " \"CodeText\": \"Ecgonine methyl ester\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 13,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00011\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631462,\n" + + " \"ParentId\": -14054803,\n" + + " \"Code\": \"LP389734-7\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389734-7\",\n" + + " \"CodeText\": \"Ecgonine methyl ester | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 3,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00011.00007\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179751871,\n" + + " \"ParentId\": 89631462,\n" + + " \"Code\": \"50592-5\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"50592-5\",\n" + + " \"CodeText\": \"EME Ur Cfm-mCnc\",\n" + + " \"Component\": \"Ecgonine methyl ester\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00011.00007.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180157065,\n" + + " \"ParentId\": 89631462,\n" + + " \"Code\": \"97154-9\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"97154-9\",\n" + + " \"CodeText\": \"EME Ur Ql Cfm\",\n" + + " \"Component\": \"Ecgonine methyl ester\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00011.00007.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180001886,\n" + + " \"ParentId\": 89631462,\n" + + " \"Code\": \"80144-9\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"80144-9\",\n" + + " \"CodeText\": \"EME Ur Ql Scn\",\n" + + " \"Component\": \"Ecgonine methyl ester\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00011.00007.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14054805,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP76346-3\",\n" + + " \"Sequence\": 15,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP76346-3\",\n" + + " \"CodeText\": \"Cocaine metabolites\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 2,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00015\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054806,\n" + + " \"ParentId\": -14054805,\n" + + " \"Code\": \"LP71227-0\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP71227-0\",\n" + + " \"CodeText\": \"Cocaine metabolites.other\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00015.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89631465,\n" + + " \"ParentId\": -14054806,\n" + + " \"Code\": \"LP389737-0\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389737-0\",\n" + + " \"CodeText\": \"Cocaine metabolites.other | Urine | Drug toxicology\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00015.00001.00001\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179779348,\n" + + " \"ParentId\": 89631465,\n" + + " \"Code\": \"53743-1\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"53743-1\",\n" + + " \"CodeText\": \"Cocaine metab.other Ur-mCnc\",\n" + + " \"Component\": \"Cocaine metabolites.other\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 9,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00015.00001.00001.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 89631417,\n" + + " \"ParentId\": -14054797,\n" + + " \"Code\": \"LP389691-9\",\n" + + " \"Sequence\": 22,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP389691-9\",\n" + + " \"CodeText\": \"Cocaine | Urine | Drug toxicology\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 12,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179417830,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"16448-3\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"16448-3\",\n" + + " \"CodeText\": \"Cocaine Ur-aCnc\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"ACnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179579989,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"3398-5\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"3398-5\",\n" + + " \"CodeText\": \"Cocaine Ur-mCnc\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179453852,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"20519-5\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"20519-5\",\n" + + " \"CodeText\": \"Cocaine Ur Cfm-mCnc\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00005\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179579912,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"3397-7\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"3397-7\",\n" + + " \"CodeText\": \"Cocaine Ur Ql\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442848,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"19360-7\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19360-7\",\n" + + " \"CodeText\": \"Cocaine Ur Ql Cfm\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442846,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"19359-9\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19359-9\",\n" + + " \"CodeText\": \"Cocaine Ur Ql Scn\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179772263,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"52953-7\",\n" + + " \"Sequence\": 9,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"52953-7\",\n" + + " \"CodeText\": \"Cocaine Ur-sCnc\",\n" + + " \"Component\": \"Cocaine\",\n" + + " \"Property\": \"SCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00009\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442873,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"19363-1\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19363-1\",\n" + + " \"CodeText\": \"Cocaine CtO Ur Cfm-mCnc\",\n" + + " \"Component\": \"Cocaine cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00010\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179442861,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"19362-3\",\n" + + " \"Sequence\": 11,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"19362-3\",\n" + + " \"CodeText\": \"Cocaine CtO Ur Scn-mCnc\",\n" + + " \"Component\": \"Cocaine cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00011\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 180062780,\n" + + " \"ParentId\": 89631417,\n" + + " \"Code\": \"86607-9\",\n" + + " \"Sequence\": 12,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"86607-9\",\n" + + " \"CodeText\": \"Cocaine/Creat Ur Cfm\",\n" + + " \"Component\": \"Cocaine/Creatinine\",\n" + + " \"Property\": \"MRto\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00010.00004.00022.00012\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -26332666,\n" + + " \"ParentId\": -26057072,\n" + + " \"Code\": \"LP29683-7\",\n" + + " \"Sequence\": 10,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP29683-7\",\n" + + " \"CodeText\": \"Drug and Toxicology Panels\",\n" + + " \"Level\": 3,\n" + + " \"LoincAncestorCount\": 144,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00010\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -26533947,\n" + + " \"ParentId\": -26332666,\n" + + " \"Code\": \"LP71231-2\",\n" + + " \"Sequence\": 32,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP71231-2\",\n" + + " \"CodeText\": \"Cocaine panel\",\n" + + " \"Level\": 4,\n" + + " \"LoincAncestorCount\": 2,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00010.00032\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89634026,\n" + + " \"ParentId\": -26533947,\n" + + " \"Code\": \"LP392055-2\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP392055-2\",\n" + + " \"CodeText\": \"Cocaine panel | Urine | Drug and Toxicology Panels\",\n" + + " \"Level\": 5,\n" + + " \"LoincAncestorCount\": 1,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00010.00032.00002\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179779384,\n" + + " \"ParentId\": 89634026,\n" + + " \"Code\": \"53747-2\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"53747-2\",\n" + + " \"CodeText\": \"Cocaine Pnl Ur\",\n" + + " \"Component\": \"Cocaine panel\",\n" + + " \"Property\": \"-\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"-\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00010.00032.00002.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " }\n" + + "]"; + + private final String CANNABINOID_QUERY_RESPONSE = "[\n" + + " {\n" + + " \"Id\": -70212289,\n" + + " \"Code\": \"LP432695-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP432695-7\",\n" + + " \"CodeText\": \"{component}\",\n" + + " \"Level\": 0,\n" + + " \"LoincAncestorCount\": 102996,\n" + + " \"PathEnumeration\": \"00001\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14010013,\n" + + " \"ParentId\": -70212289,\n" + + " \"Code\": \"LP29693-6\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP29693-6\",\n" + + " \"CodeText\": \"Laboratory\",\n" + + " \"Level\": 1,\n" + + " \"LoincAncestorCount\": 62659,\n" + + " \"PathEnumeration\": \"00001.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -26057072,\n" + + " \"ParentId\": -14010013,\n" + + " \"Code\": \"LP7790-1\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP7790-1\",\n" + + " \"CodeText\": \"Drug toxicology\",\n" + + " \"Level\": 2,\n" + + " \"LoincAncestorCount\": 8636,\n" + + " \"PathEnumeration\": \"00001.00001.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14054262,\n" + + " \"ParentId\": -26057072,\n" + + " \"Code\": \"LP18046-0\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP18046-0\",\n" + + " \"CodeText\": \"Drugs\",\n" + + " \"Level\": 3,\n" + + " \"LoincAncestorCount\": 6367,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14055272,\n" + + " \"ParentId\": -14054262,\n" + + " \"Code\": \"LP31449-9\",\n" + + " \"Sequence\": 42,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP31449-9\",\n" + + " \"CodeText\": \"Psychiatric drugs\",\n" + + " \"Level\": 4,\n" + + " \"LoincAncestorCount\": 959,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14055411,\n" + + " \"ParentId\": -14055272,\n" + + " \"Code\": \"LP14348-4\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP14348-4\",\n" + + " \"CodeText\": \"Ethanol\",\n" + + " \"Level\": 5,\n" + + " \"LoincAncestorCount\": 48,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": -14055412,\n" + + " \"ParentId\": -14055411,\n" + + " \"Code\": \"LP16119-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP16119-7\",\n" + + " \"CodeText\": \"Disulfiram\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 3,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89633309,\n" + + " \"ParentId\": -14055412,\n" + + " \"Code\": \"LP391429-0\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP391429-0\",\n" + + " \"CodeText\": \"Disulfiram | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 2,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00001.00002\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 180125050,\n" + + " \"ParentId\": 89633309,\n" + + " \"Code\": \"9357-5\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"9357-5\",\n" + + " \"CodeText\": \"Disulfiram Ur-mCnc\",\n" + + " \"Component\": \"Disulfiram\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00001.00002.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179420963,\n" + + " \"ParentId\": 89633309,\n" + + " \"Code\": \"16781-7\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"16781-7\",\n" + + " \"CodeText\": \"Disulfiram Ur Ql\",\n" + + " \"Component\": \"Disulfiram\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00001.00002.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": -14055413,\n" + + " \"ParentId\": -14055411,\n" + + " \"Code\": \"LP36909-7\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP36909-7\",\n" + + " \"CodeText\": \"Ethyl glucuronide\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 10,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": true,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 89633312,\n" + + " \"ParentId\": -14055413,\n" + + " \"Code\": \"LP391431-6\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP391431-6\",\n" + + " \"CodeText\": \"Ethyl glucuronide | Urine | Drug toxicology\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 8,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179702972,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"45324-1\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"45324-1\",\n" + + " \"CodeText\": \"Ethyl glucuronide Ur-mCnc\",\n" + + " \"Component\": \"Ethyl glucuronide\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179819422,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"58378-1\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"58378-1\",\n" + + " \"CodeText\": \"Ethyl glucuronide Ur Cfm-mCnc\",\n" + + " \"Component\": \"Ethyl glucuronide\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179792200,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"55349-5\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"55349-5\",\n" + + " \"CodeText\": \"Ethyl glucuronide Ur Ql\",\n" + + " \"Component\": \"Ethyl glucuronide\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179819413,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"58377-3\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"58377-3\",\n" + + " \"CodeText\": \"Ethyl glucuronide Ur Ql Cfm\",\n" + + " \"Component\": \"Ethyl glucuronide\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179819399,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"58375-7\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"58375-7\",\n" + + " \"CodeText\": \"Ethyl glucuronide Ur Ql Scn\",\n" + + " \"Component\": \"Ethyl glucuronide\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00005\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179978208,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"77769-8\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"77769-8\",\n" + + " \"CodeText\": \"Ethyl glucuronide CtO Ur Cfm-mCnc\",\n" + + " \"Component\": \"Ethyl glucuronide cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179992387,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"79239-0\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"79239-0\",\n" + + " \"CodeText\": \"Ethyl glucuronide CtO Ur Scn-mCnc\",\n" + + " \"Component\": \"Ethyl glucuronide cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179819409,\n" + + " \"ParentId\": 89633312,\n" + + " \"Code\": \"58376-5\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"58376-5\",\n" + + " \"CodeText\": \"Ethyl glucuronide/Creat Ur\",\n" + + " \"Component\": \"Ethyl glucuronide/Creatinine\",\n" + + " \"Property\": \"MRto\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 8,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00004.00005.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 89633305,\n" + + " \"ParentId\": -14055411,\n" + + " \"Code\": \"LP391425-8\",\n" + + " \"Sequence\": 14,\n" + + " \"HierarchyId\": 15,\n" + + " \"PartNumber\": \"LP391425-8\",\n" + + " \"CodeText\": \"Ethanol | Urine | Drug toxicology\",\n" + + " \"Level\": 6,\n" + + " \"LoincAncestorCount\": 9,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014\",\n" + + " \"ShowLink\": false,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": true,\n" + + " \"IsLoinc\": false\n" + + " },\n" + + " {\n" + + " \"Id\": 179801965,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"5645-7\",\n" + + " \"Sequence\": 1,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"5645-7\",\n" + + " \"CodeText\": \"Ethanol Ur-mCnc\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00001\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179716113,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"46983-3\",\n" + + " \"Sequence\": 2,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"46983-3\",\n" + + " \"CodeText\": \"Ethanol Ur Cfm-mCnc\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00002\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179801823,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"5644-0\",\n" + + " \"Sequence\": 3,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"5644-0\",\n" + + " \"CodeText\": \"Ethanol Ur Ql\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00003\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179581711,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"34180-0\",\n" + + " \"Sequence\": 4,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"34180-0\",\n" + + " \"CodeText\": \"Ethanol Ur Ql Cfm\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00004\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179673463,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"42242-8\",\n" + + " \"Sequence\": 5,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"42242-8\",\n" + + " \"CodeText\": \"Ethanol Ur Ql Scn\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"PrThr\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Ord\",\n" + + " \"Method\": \"Screen\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00005\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179473241,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"22745-4\",\n" + + " \"Sequence\": 6,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"22745-4\",\n" + + " \"CodeText\": \"Ethanol Ur-sCnc\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"SCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00006\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179595375,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"35664-2\",\n" + + " \"Sequence\": 7,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"35664-2\",\n" + + " \"CodeText\": \"Ethanol ?Tm Ur-mCnc\",\n" + + " \"Component\": \"Ethanol\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"XXX\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00007\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179978196,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"77768-0\",\n" + + " \"Sequence\": 8,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"77768-0\",\n" + + " \"CodeText\": \"Ethanol CtO Ur Cfm-mCnc\",\n" + + " \"Component\": \"Ethanol cutoff\",\n" + + " \"Property\": \"MCnc\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Method\": \"Confirm\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00008\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " },\n" + + " {\n" + + " \"Id\": 179819261,\n" + + " \"ParentId\": 89633305,\n" + + " \"Code\": \"58356-7\",\n" + + " \"Sequence\": 9,\n" + + " \"HierarchyId\": 15,\n" + + " \"LoincNumber\": \"58356-7\",\n" + + " \"CodeText\": \"Ethanol/Creat Ur\",\n" + + " \"Component\": \"Ethanol/Creatinine\",\n" + + " \"Property\": \"MRto\",\n" + + " \"Timing\": \"Pt\",\n" + + " \"System\": \"Urine\",\n" + + " \"Scale\": \"Qn\",\n" + + " \"Level\": 7,\n" + + " \"LoincAncestorCount\": 0,\n" + + " \"PathEnumeration\": \"00001.00001.00004.00008.00042.00004.00014.00009\",\n" + + " \"ShowLink\": true,\n" + + " \"HasChildren\": false,\n" + + " \"HasLoincChildren\": false,\n" + + " \"IsLoinc\": true\n" + + " }\n" + + "]"; +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/RxMixWorflowProcessorIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/RxMixWorflowProcessorIT.java new file mode 100644 index 000000000..8a535931c --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/codesystem/RxMixWorflowProcessorIT.java @@ -0,0 +1,81 @@ +package org.opencds.cqf.tooling.operations.codesystem; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ValueSet; +import org.opencds.cqf.tooling.constants.Terminology; +import org.opencds.cqf.tooling.operations.codesystem.rxnorm.RxMixWorkflowProcessor; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class RxMixWorflowProcessorIT { + + @Test + void testOpioidAnalgesicsWithAmbulatoryMisusePotential() { + // Single Input with exclusion filters + String rulesText = "https://mor.nlm.nih.gov/RxMix/ Script:\\r\\nStep 1a \\r\\nCreate Batch text input file (SCT-Opioids.txt) with following SCT identifier (for the concept \\\"Product containing opioid receptor agonist (product)\\\") as an input within the file: \\r\\n360204007 \\r\\n\\r\\nStep 1b\\r\\nSubmit batch job using the above SCT-Opioids.txt file to following workflow by uploading file (SCT-Opioid-wf.config) with the following in the file: <WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>findClassById</function><level>0</level><paramSize>1</paramSize><param order ='0'>?</param></FS><FS><service>NOINPUT</service><function>getClassMembers</function><level>1</level><paramSize>5</paramSize><param order ='0'>?</param><param order ='1'>SNOMEDCT</param><param order ='2'>isa_disposition</param><param order ='3'>0</param><param order ='4'>IN,MIN,PIN</param></FS><FS><service>NOINPUT</service><function>getRelatedByType</function><level>2</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE>\\r\\nThis will produce a result file with all Opioid clinical drugs included\\r\\n\\r\\nStep 2\\r\\nTo remove all cough and bowel transit formulation codes and to remove the injectable codes filter out all codes with the following strings:\\r\\nIngredient strings: \\r\\nGuaifenesin, Chlorpheniramine, Pseudoephedrine, Brompheniramine, Phenylephrine, Phenylpropanolamine, Promethazine, Bromodiphenhydramine, guaiacolsulfonate, homatropine\\r\\nForm strings:\\r\\ninject, cartridge, syringe"; + String workflowLibrary = "<WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>findClassById</function><level>0</level><paramSize>1</paramSize><param order ='0'>?</param></FS><FS><service>NOINPUT</service><function>getClassMembers</function><level>1</level><paramSize>5</paramSize><param order ='0'>?</param><param order ='1'>SNOMEDCT</param><param order ='2'>isa_disposition</param><param order ='3'>0</param><param order ='4'>IN,MIN,PIN</param></FS><FS><service>NOINPUT</service><function>getRelatedByType</function><level>2</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE>"; + String input = "360204007"; + String excludeFilter = "Guaifenesin, Chlorpheniramine, Pseudoephedrine, Brompheniramine, Phenylephrine, Phenylpropanolamine, Promethazine, Bromodiphenhydramine, guaiacolsulfonate, homatropine, inject, cartridge, syringe"; + + RxMixWorkflowProcessor rxMixWorkflowProcessor = new RxMixWorkflowProcessor(); + rxMixWorkflowProcessor.setRulesText(rulesText); + rxMixWorkflowProcessor.setWorkflow(workflowLibrary); + rxMixWorkflowProcessor.setInput(input); + rxMixWorkflowProcessor.setExcludeFilter(excludeFilter); + rxMixWorkflowProcessor.setFhirContext(FhirContext.forR4Cached()); + + IBaseResource returnVs = rxMixWorkflowProcessor.getValueSet(); + Assert.assertEquals(rxMixWorkflowProcessor.getExcludeFilters().size(), 13); + Assert.assertTrue(returnVs instanceof ValueSet); + Assert.assertTrue(((ValueSet) returnVs).hasCompose()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().hasInclude()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().getIncludeFirstRep().hasSystem()); + Assert.assertEquals(((ValueSet) returnVs).getCompose().getIncludeFirstRep().getSystem(), Terminology.RXNORM_SYSTEM_URL); + } + + @Test + void testExtendedReleaseOpioidsWithAmbulatoryMisusePotential() { + // Single Input with inclusion and exclusion filters (testing with abnormal whitespaces) + String rulesText = "Step 1:\\r\\nExpand the value set \\\"Opioids with Opioid analgesic with ambulatory misuse potential\\\" and then remove concepts that are long acting:\\r\\n\\r\\nStep 2:\\r\\nFilter the result to only select concepts that are a drug with Methadone Ingredient, or have one of the following dose forms: 316943 Extended Release Oral Capsule, 316945 Extended Release Oral Tablet, 316946 Extended Release Oral Tablet, 316987 Transdermal System. To do this filter the list by only including descriptions that have one of the following strings: \\\"Extended Release\\\" OR \\\"Transdermal\\\" OR \\\"Methadone\\\"."; + String workflowLibrary = "<WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>findClassById</function><level>0</level><paramSize>1</paramSize><param order ='0'>?</param></FS><FS><service>NOINPUT</service><function>getClassMembers</function><level>1</level><paramSize>5</paramSize><param order ='0'>?</param><param order ='1'>SNOMEDCT</param><param order ='2'>isa_disposition</param><param order ='3'>0</param><param order ='4'>IN,MIN,PIN</param></FS><FS><service>NOINPUT</service><function>getRelatedByType</function><level>2</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE>"; + String input = "360204007"; + String includeFilter = "Extended Release , Transdermal,Methadone"; + String excludeFilter = "Guaifenesin, Chlorpheniramine, Pseudoephedrine, Brompheniramine, Phenylephrine, Phenylpropanolamine, Promethazine, Bromodiphenhydramine, guaiacolsulfonate, homatropine, inject, cartridge, syringe"; + + RxMixWorkflowProcessor rxMixWorkflowProcessor = new RxMixWorkflowProcessor(); + rxMixWorkflowProcessor.setRulesText(rulesText); + rxMixWorkflowProcessor.setWorkflow(workflowLibrary); + rxMixWorkflowProcessor.setInput(input); + rxMixWorkflowProcessor.setIncludeFilter(includeFilter); + rxMixWorkflowProcessor.setExcludeFilter(excludeFilter); + rxMixWorkflowProcessor.setFhirContext(FhirContext.forR4Cached()); + + IBaseResource returnVs = rxMixWorkflowProcessor.getValueSet(); + Assert.assertEquals(rxMixWorkflowProcessor.getIncludeFilters().size(), 3); + Assert.assertEquals(rxMixWorkflowProcessor.getExcludeFilters().size(), 13); + Assert.assertTrue(returnVs instanceof ValueSet); + Assert.assertTrue(((ValueSet) returnVs).hasCompose()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().hasInclude()); + } + + @Test + void testFentanylTypeMedications() { + // Multiple Inputs (testing with abnormal whitespaces) + String rulesText = "https://mor.nlm.nih.gov/RxMix/ Script: \\r\\nStep 1 Upload to RxMix a workflow config file named GetRelatedByType.config containing the following workflow text: <WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>getRelatedByType</function><level>0</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE> \\r\\n\\r\\nStep 2 Create Batch text input file (Ingredients.txt) with following RxNorm Fentanyl-type ingredient codes representing fentanyl, sufentanil, alfentanil, remifentanil as an input within the file:\\r\\n4337\\r\\n56795\\r\\n480\\r\\n73032\\r\\n\\r\\nStep 3 Upload the batch text input file Ingredients.txt created in step 2. \\r\\n\\r\\nStep 4 Submit the batch which will run the workflow using the input codes to generate a combined set of all the concepts needed..\\r\\n"; + String workflowLibrary = "<WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>getRelatedByType</function><level>0</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE>"; + String input = "4337, 56795,480, 73032"; + + RxMixWorkflowProcessor rxMixWorkflowProcessor = new RxMixWorkflowProcessor(); + rxMixWorkflowProcessor.setRulesText(rulesText); + rxMixWorkflowProcessor.setWorkflow(workflowLibrary); + rxMixWorkflowProcessor.setInput(input); + rxMixWorkflowProcessor.setFhirContext(FhirContext.forR4Cached()); + + IBaseResource returnVs = rxMixWorkflowProcessor.getValueSet(); + Assert.assertEquals(rxMixWorkflowProcessor.getInputs().size(), 4); + Assert.assertTrue(returnVs instanceof ValueSet); + Assert.assertTrue(((ValueSet) returnVs).hasCompose()); + Assert.assertTrue(((ValueSet) returnVs).getCompose().hasInclude()); + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/dateroller/DateRollerOperationIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/dateroller/DateRollerOperationIT.java new file mode 100644 index 000000000..40942c541 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/dateroller/DateRollerOperationIT.java @@ -0,0 +1,373 @@ +package org.opencds.cqf.tooling.operations.dateroller; + +import ca.uhn.fhir.context.FhirContext; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Duration; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Period; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.TimeZone; + +public class DateRollerOperationIT { + private final FhirContext fhirContext = FhirContext.forR4Cached(); + private final Date today = new Date(); + private final Date todayMinus40Days = DateUtils.addDays(today, -40); + private final Date todayMinus41Days = DateUtils.addDays(today, -41); + private final Date todayMinus50Days = DateUtils.addDays(today, -50); + private final String CDSHooksWithPrefetchAndNulls = "{\n" + + " \"hookInstance\": \"6bc883b2-b795-4dcb-b661-34884a31d472\",\n" + + " \"fhirServer\": \"http://localhost:8080/fhir\",\n" + + " \"hook\": \"order-sign\",\n" + + " \"context\": {\n" + + " \"userId\": \"Practitioner/example\",\n" + + " \"patientId\": \"Patient/example-rec-01-true-make-recommendations\",\n" + + " \"draftOrders\": {\n" + + " \"resourceType\": \"Bundle\",\n" + + " \"type\": \"collection\",\n" + + " \"entry\": [\n" + + " {\n" + + " \"resource\": {\n" + + " \"resourceType\": \"MedicationRequest\",\n" + + " \"id\": \"05f8cb26-2eb6-4124-b65d-bb1f13e21c49\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"dateLastUpdated\",\n" + + " \"valueDateTime\": \"2022-10-10\"\n" + + " },\n" + + " {\n" + + " \"url\": \"frequency\",\n" + + " \"valueDuration\": {\n" + + " \"value\": 30.0,\n" + + " \"unit\": \"days\",\n" + + " \"system\": \"http://unitsofmeasure.org\",\n" + + " \"code\": \"d\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"intent\": \"order\",\n" + + " \"category\": [\n" + + " {\n" + + " \"coding\": [\n" + + " {\n" + + " \"system\": \"http://terminology.hl7.org/CodeSystem/medicationrequest-category\",\n" + + " \"code\": \"community\",\n" + + " \"display\": \"Community\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"medicationCodeableConcept\": {\n" + + " \"coding\": [\n" + + " {\n" + + " \"system\": \"http://www.nlm.nih.gov/research/umls/rxnorm\",\n" + + " \"code\": \"1010603\",\n" + + " \"display\": \"Suboxone 2 MG / 0.5 MG Sublingual Film\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"subject\": {\n" + + " \"reference\": \"Patient/example-rec-01-true-make-recommendations\"\n" + + " },\n" + + " \"encounter\": {\n" + + " \"reference\": \"Encounter/example-rec-01-in-outpatient-opioid-context\"\n" + + " },\n" + + " \"authoredOn\": \"2022-10-10\",\n" + + " \"dosageInstruction\": [\n" + + " {\n" + + " \"timing\": {\n" + + " \"repeat\": {\n" + + " \"frequency\": 1,\n" + + " \"period\": 1.0,\n" + + " \"periodUnit\": \"d\"\n" + + " }\n" + + " },\n" + + " \"asNeededBoolean\": false,\n" + + " \"doseAndRate\": [\n" + + " {\n" + + " \"doseQuantity\": {\n" + + " \"value\": 1.0,\n" + + " \"unit\": \"film\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"dispenseRequest\": {\n" + + " \"validityPeriod\": {\n" + + " \"start\": \"2022-10-10T00:00:00-06:00\",\n" + + " \"end\": \"2023-01-10T00:00:00-07:00\"\n" + + " },\n" + + " \"numberOfRepeatsAllowed\": 1,\n" + + " \"expectedSupplyDuration\": {\n" + + " \"value\": 27,\n" + + " \"unit\": \"days\",\n" + + " \"system\": \"http://unitsofmeasure.org\",\n" + + " \"code\": \"d\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"prefetch\": {\n" + + " \"item1\": {\n" + + " \"resourceType\": \"Patient\",\n" + + " \"id\": \"example-rec-01-true-make-recommendations\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"dateLastUpdated\",\n" + + " \"valueDateTime\": \"2022-10-10\"\n" + + " },\n" + + " {\n" + + " \"url\": \"frequency\",\n" + + " \"valueDuration\": {\n" + + " \"value\": 30.0,\n" + + " \"unit\": \"days\",\n" + + " \"system\": \"http://unitsofmeasure.org\",\n" + + " \"code\": \"d\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"birthDate\": \"2002-10-10\"\n" + + " },\n" + + " \"item2\": null,\n" + + " \"item3\": null,\n" + + " \"item4\": null,\n" + + " \"item5\": null,\n" + + " \"item6\": null,\n" + + " \"item7\": null,\n" + + " \"item8\": null,\n" + + " \"item9\": null,\n" + + " \"item10\": null\n" + + " }\n" + + "}"; + private final String CDSHooksWithoutPrefetch = "{\n" + + " \"hookInstance\": \"6bc883b2-b795-4dcb-b661-34884a31d472\",\n" + + " \"fhirServer\": \"http://localhost:8080/fhir\",\n" + + " \"hook\": \"order-sign\",\n" + + " \"context\": {\n" + + " \"userId\": \"Practitioner/example\",\n" + + " \"patientId\": \"Patient/example-rec-01-true-make-recommendations\",\n" + + " \"draftOrders\": {\n" + + " \"resourceType\": \"Bundle\",\n" + + " \"type\": \"collection\",\n" + + " \"entry\": [\n" + + " {\n" + + " \"resource\": {\n" + + " \"resourceType\": \"MedicationRequest\",\n" + + " \"id\": \"05f8cb26-2eb6-4124-b65d-bb1f13e21c49\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/dataDateRoller\",\n" + + " \"extension\": [\n" + + " {\n" + + " \"url\": \"dateLastUpdated\",\n" + + " \"valueDateTime\": \"2022-10-10\"\n" + + " },\n" + + " {\n" + + " \"url\": \"frequency\",\n" + + " \"valueDuration\": {\n" + + " \"value\": 30.0,\n" + + " \"unit\": \"days\",\n" + + " \"system\": \"http://unitsofmeasure.org\",\n" + + " \"code\": \"d\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"status\": \"active\",\n" + + " \"intent\": \"order\",\n" + + " \"category\": [\n" + + " {\n" + + " \"coding\": [\n" + + " {\n" + + " \"system\": \"http://terminology.hl7.org/CodeSystem/medicationrequest-category\",\n" + + " \"code\": \"community\",\n" + + " \"display\": \"Community\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"medicationCodeableConcept\": {\n" + + " \"coding\": [\n" + + " {\n" + + " \"system\": \"http://www.nlm.nih.gov/research/umls/rxnorm\",\n" + + " \"code\": \"1010603\",\n" + + " \"display\": \"Suboxone 2 MG / 0.5 MG Sublingual Film\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"subject\": {\n" + + " \"reference\": \"Patient/example-rec-01-true-make-recommendations\"\n" + + " },\n" + + " \"encounter\": {\n" + + " \"reference\": \"Encounter/example-rec-01-in-outpatient-opioid-context\"\n" + + " },\n" + + " \"authoredOn\": \"2022-10-10\",\n" + + " \"dosageInstruction\": [\n" + + " {\n" + + " \"timing\": {\n" + + " \"repeat\": {\n" + + " \"frequency\": 1,\n" + + " \"period\": 1.0,\n" + + " \"periodUnit\": \"d\"\n" + + " }\n" + + " },\n" + + " \"asNeededBoolean\": false,\n" + + " \"doseAndRate\": [\n" + + " {\n" + + " \"doseQuantity\": {\n" + + " \"value\": 1.0,\n" + + " \"unit\": \"film\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"dispenseRequest\": {\n" + + " \"validityPeriod\": {\n" + + " \"start\": \"2022-10-10T00:00:00-06:00\",\n" + + " \"end\": \"2023-01-10T00:00:00-07:00\"\n" + + " },\n" + + " \"numberOfRepeatsAllowed\": 1,\n" + + " \"expectedSupplyDuration\": {\n" + + " \"value\": 27,\n" + + " \"unit\": \"days\",\n" + + " \"system\": \"http://unitsofmeasure.org\",\n" + + " \"code\": \"d\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + @Test + void testDateRollerDateTimeElement() { + RollTestDates dateRoller = new RollTestDates(); + + // dateTime - same day as dateLastUpdated + Observation observation = new Observation(); + observation.addExtension(getDateRollerExtension()); + observation.setEffective(new DateTimeType(todayMinus40Days)); + boolean result = dateRoller.getAllDateElements(fhirContext, observation, dateRoller.getDateClasses(fhirContext)); + Assert.assertTrue(result); + Assert.assertTrue(DateUtils.isSameDay(observation.getEffectiveDateTimeType().getValue(), today)); + Assert.assertTrue(DateUtils.isSameDay( + ((DateTimeType) observation.getExtensionByUrl(RollTestDates.DATEROLLER_EXT_URL) + .getExtensionByUrl("dateLastUpdated").getValue()).getValue(), today)); + } + + @Test + void testDateRollerInstantElementTenDaysBeforeDLU() { + RollTestDates dateRoller = new RollTestDates(); + + // instant - 10 days before dateLastUpdated + Observation observation = new Observation(); + observation.addExtension(getDateRollerExtension()); + observation.setIssued(todayMinus50Days); + boolean result = dateRoller.getAllDateElements(fhirContext, observation, dateRoller.getDateClasses(fhirContext)); + Assert.assertTrue(result); + Assert.assertTrue(DateUtils.isSameDay(observation.getIssued(), DateUtils.addDays(today, -10))); + } + + @Test + void testDateRollerPeriodElementUTCTimezone() { + RollTestDates dateRoller = new RollTestDates(); + + // period - start is 1 day before dateLastUpdated, end is same day as dateLastUpdated - UTC TimeZone (0 offset) + Observation observation = new Observation(); + observation.addExtension(getDateRollerExtension()); + DateTimeType start = new DateTimeType(todayMinus41Days); + start.setTimeZone(TimeZone.getTimeZone("UTC")); + DateTimeType end = new DateTimeType(todayMinus40Days); + end.setTimeZone(TimeZone.getTimeZone("UTC")); + observation.setEffective(new Period().setStartElement(start).setEndElement(end)); + boolean result = dateRoller.getAllDateElements( + fhirContext, observation, dateRoller.getDateClasses(fhirContext)); + Assert.assertTrue(result); + Assert.assertTrue(DateUtils.isSameDay( + observation.getEffectivePeriod().getStart(), DateUtils.addDays(today, -1))); + Assert.assertTrue(DateUtils.isSameDay(observation.getEffectivePeriod().getEnd(), today)); + // Check that TimeZone is preserved + Assert.assertEquals(observation.getEffectivePeriod().getStartElement().getTimeZone().getRawOffset(), 0); + } + + @Test + void testDateRollerBackboneElement() { + RollTestDates dateRoller = new RollTestDates(); + + // Backbone element Observation.component.valueDate + Observation observation = new Observation(); + observation.addExtension(getDateRollerExtension()); + observation.addComponent().setValue(new DateTimeType(todayMinus50Days)); + boolean result = dateRoller.getAllDateElements(fhirContext, observation, dateRoller.getDateClasses(fhirContext)); + Assert.assertTrue(result); + Assert.assertTrue(DateUtils.isSameDay(observation.getComponentFirstRep().getValueDateTimeType().getValue(), + DateUtils.addDays(today, -10))); + } + + @Test + void testCdsHooksRequestPrefetch() { + Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); + JsonObject request = gson.fromJson(CDSHooksWithPrefetchAndNulls, JsonObject.class); + String oldRequest = gson.toJson(request); + RollTestDates dateRoller = new RollTestDates(); + dateRoller.setFhirContext(fhirContext); + dateRoller.getUpdatedRequest(request, gson); + // TODO: more extensive testing to ensure the update is correct would be nice - need to revisit better method + // Would need to build request dynamically to reliably test expected dates or some other method + // For now, just ensuring that the requests are not the same is sufficient - desk checking has been performed + Assert.assertNotEquals(gson.toJson(request), oldRequest); + // ensure null values are preserved + Assert.assertTrue(request.getAsJsonObject("prefetch").get("item2").isJsonNull()); + } + + @Test + void testCdsHooksRequestNoPrefetch() { + Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); + JsonObject request = gson.fromJson(CDSHooksWithoutPrefetch, JsonObject.class); + String oldRequest = gson.toJson(request); + RollTestDates dateRoller = new RollTestDates(); + dateRoller.setFhirContext(fhirContext); + dateRoller.getUpdatedRequest(request, gson); + // TODO: more extensive testing to ensure the update is correct would be nice - need to revisit better method + // Would need to build request dynamically to reliably test expected dates or some other method + // For now, just ensuring that the requests are not the same is sufficient - desk checking has been performed + Assert.assertNotEquals(gson.toJson(request), oldRequest); + // ensure no prefetch + Assert.assertFalse(request.has("prefetch")); + } + + private Extension getDateRollerExtension() { + Duration frequency = new Duration(); + frequency.setValue(30).setUnit("days"); + Extension dateRollerExtension = new Extension(RollTestDates.DATEROLLER_EXT_URL); + dateRollerExtension.addExtension("dateLastUpdated", new DateTimeType(todayMinus40Days)); + dateRollerExtension.addExtension("frequency", frequency); + return dateRollerExtension; + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryGenerationIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryGenerationIT.java new file mode 100644 index 000000000..cf8f11df4 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryGenerationIT.java @@ -0,0 +1,217 @@ +package org.opencds.cqf.tooling.operations.library; + +import ca.uhn.fhir.context.FhirContext; +import org.cqframework.cql.cql2elm.CqlTranslator; +import org.cqframework.cql.cql2elm.CqlTranslatorOptions; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.ModelManager; +import org.cqframework.cql.elm.requirements.fhir.DataRequirementsProcessor; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class LibraryGenerationIT { + private final ModelManager modelManager = new ModelManager(); + private final LibraryManager libraryManager = new LibraryManager(modelManager); + private final DataRequirementsProcessor dataRequirementsProcessor = new DataRequirementsProcessor(); + CqlTranslatorOptions options = CqlTranslatorOptions.defaultOptions(); + + @Test + void testSimpleDSTU3LibraryGeneration() { + LibraryGenerator libraryGenerator = new LibraryGenerator(); + libraryGenerator.setFhirContext(FhirContext.forDstu3Cached()); + CqlTranslator translator = CqlTranslator.fromText(DSTU3PartialFhirHelpers, modelManager, libraryManager, + options.getOptions().toArray(new CqlTranslatorOptions.Options[]{})); + IBaseResource library = libraryGenerator.resolveFhirLibrary(translator, + dataRequirementsProcessor.gatherDataRequirements(libraryManager, translator.getTranslatedLibrary(), + options, null, false), R4PartialFhirHelpers); + Assert.assertTrue(library instanceof org.hl7.fhir.dstu3.model.Library); + org.hl7.fhir.dstu3.model.Library r4Library = (org.hl7.fhir.dstu3.model.Library) library; + Assert.assertTrue(r4Library.hasId()); + Assert.assertTrue(r4Library.hasName()); + Assert.assertTrue(r4Library.hasVersion()); + Assert.assertTrue(r4Library.hasExperimental()); + Assert.assertTrue(r4Library.hasStatus()); + Assert.assertTrue(r4Library.hasType()); + Assert.assertTrue(r4Library.hasContent()); + Assert.assertEquals(r4Library.getContent().size(), 3); + } + + @Test + void testSimpleR4LibraryGeneration() { + LibraryGenerator libraryGenerator = new LibraryGenerator(); + libraryGenerator.setFhirContext(FhirContext.forR4Cached()); + CqlTranslator translator = CqlTranslator.fromText(R4PartialFhirHelpers, modelManager, libraryManager, + options.getOptions().toArray(new CqlTranslatorOptions.Options[]{})); + IBaseResource library = libraryGenerator.resolveFhirLibrary(translator, + dataRequirementsProcessor.gatherDataRequirements(libraryManager, translator.getTranslatedLibrary(), + options, null, false), R4PartialFhirHelpers); + Assert.assertTrue(library instanceof org.hl7.fhir.r4.model.Library); + org.hl7.fhir.r4.model.Library r4Library = (org.hl7.fhir.r4.model.Library) library; + Assert.assertTrue(r4Library.hasId()); + Assert.assertTrue(r4Library.hasName()); + Assert.assertTrue(r4Library.hasVersion()); + Assert.assertTrue(r4Library.hasExperimental()); + Assert.assertTrue(r4Library.hasStatus()); + Assert.assertTrue(r4Library.hasType()); + Assert.assertTrue(r4Library.hasContent()); + Assert.assertEquals(r4Library.getContent().size(), 3); + } + + @Test + void testSimpleR5LibraryGeneration() { + LibraryGenerator libraryGenerator = new LibraryGenerator(); + libraryGenerator.setFhirContext(FhirContext.forR5Cached()); + CqlTranslator translator = CqlTranslator.fromText(R4PartialFhirHelpers, modelManager, libraryManager, + options.getOptions().toArray(new CqlTranslatorOptions.Options[]{})); + IBaseResource library = libraryGenerator.resolveFhirLibrary(translator, + dataRequirementsProcessor.gatherDataRequirements(libraryManager, translator.getTranslatedLibrary(), + options, null, false), R4PartialFhirHelpers); + Assert.assertTrue(library instanceof org.hl7.fhir.r5.model.Library); + org.hl7.fhir.r5.model.Library r4Library = (org.hl7.fhir.r5.model.Library) library; + Assert.assertTrue(r4Library.hasId()); + Assert.assertTrue(r4Library.hasName()); + Assert.assertTrue(r4Library.hasVersion()); + Assert.assertTrue(r4Library.hasExperimental()); + Assert.assertTrue(r4Library.hasStatus()); + Assert.assertTrue(r4Library.hasType()); + Assert.assertTrue(r4Library.hasContent()); + Assert.assertEquals(r4Library.getContent().size(), 3); + } + + private final String DSTU3PartialFhirHelpers = "library FHIRHelpers version '1.8'\n" + + "\n" + + "using FHIR version '1.8'\n" + + "\n" + + "define function ToInterval(period FHIR.Period):\n" + + " Interval[period.\"start\".value, period.\"end\".value]\n" + + "\n" + + "define function ToQuantity(quantity FHIR.Quantity):\n" + + " System.Quantity { value: quantity.value.value, unit: quantity.unit.value }\n" + + "\n" + + "define function ToInterval(range FHIR.Range):\n" + + " Interval[ToQuantity(range.low), ToQuantity(range.high)]\n" + + "\n" + + "define function ToCode(coding FHIR.Coding):\n" + + " System.Code {\n" + + " code: coding.code.value,\n" + + " system: coding.system.value,\n" + + " version: coding.version.value,\n" + + " display: coding.display.value\n" + + " }\n" + + "\n" + + "define function ToConcept(concept FHIR.CodeableConcept):\n" + + " System.Concept {\n" + + " codes: concept.coding C return ToCode(C),\n" + + " display: concept.text.value\n" + + " }"; + + private final String R4PartialFhirHelpers = "library FHIRHelpers version '4.0.1'\n" + + "\n" + + "using FHIR version '4.0.1'\n" + + "\n" + + "define function ToInterval(period FHIR.Period):\n" + + " if period is null then\n" + + " null\n" + + " else\n" + + " if period.\"start\" is null then\n" + + " Interval(period.\"start\".value, period.\"end\".value]\n" + + " else\n" + + " Interval[period.\"start\".value, period.\"end\".value]\n" + + "\n" + + "define function ToCalendarUnit(unit System.String):\n" + + " case unit\n" + + " when 'ms' then 'millisecond'\n" + + " when 's' then 'second'\n" + + " when 'min' then 'minute'\n" + + " when 'h' then 'hour'\n" + + " when 'd' then 'day'\n" + + " when 'wk' then 'week'\n" + + " when 'mo' then 'month'\n" + + " when 'a' then 'year'\n" + + " else unit\n" + + " end\n" + + "\n" + + "define function ToQuantity(quantity FHIR.Quantity):\n" + + " case\n" + + " when quantity is null then null\n" + + " when quantity.value is null then null\n" + + " when quantity.comparator is not null then\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.ComparatorQuantityNotSupported', 'Error', 'FHIR Quantity value has a comparator and cannot be converted to a System.Quantity value.')\n" + + " when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org'\n" + + " or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then\n" + + " System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) }\n" + + " else\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')')\n" + + " end\n" + + "\n" + + "define function ToQuantityIgnoringComparator(quantity FHIR.Quantity):\n" + + " case\n" + + " when quantity is null then null\n" + + " when quantity.value is null then null\n" + + " when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org'\n" + + " or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then\n" + + " System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) }\n" + + " else\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')')\n" + + " end\n" + + "\n" + + "define function ToInterval(quantity FHIR.Quantity):\n" + + " if quantity is null then null else\n" + + " case quantity.comparator.value\n" + + " when '<' then\n" + + " Interval[\n" + + " null,\n" + + " ToQuantityIgnoringComparator(quantity)\n" + + " )\n" + + " when '<=' then\n" + + " Interval[\n" + + " null,\n" + + " ToQuantityIgnoringComparator(quantity)\n" + + " ]\n" + + " when '>=' then\n" + + " Interval[\n" + + " ToQuantityIgnoringComparator(quantity),\n" + + " null\n" + + " ]\n" + + " when '>' then\n" + + " Interval(\n" + + " ToQuantityIgnoringComparator(quantity),\n" + + " null\n" + + " ]\n" + + " else\n" + + " Interval[ToQuantity(quantity), ToQuantity(quantity)]\n" + + " end\n" + + "\n" + + "define function ToRatio(ratio FHIR.Ratio):\n" + + " if ratio is null then\n" + + " null\n" + + " else\n" + + " System.Ratio { numerator: ToQuantity(ratio.numerator), denominator: ToQuantity(ratio.denominator) }\n" + + "\n" + + "define function ToInterval(range FHIR.Range):\n" + + " if range is null then\n" + + " null\n" + + " else\n" + + " Interval[ToQuantity(range.low), ToQuantity(range.high)]\n" + + "\n" + + "define function ToCode(coding FHIR.Coding):\n" + + " if coding is null then\n" + + " null\n" + + " else\n" + + " System.Code {\n" + + " code: coding.code.value,\n" + + " system: coding.system.value,\n" + + " version: coding.version.value,\n" + + " display: coding.display.value\n" + + " }\n" + + "\n" + + "define function ToConcept(concept FHIR.CodeableConcept):\n" + + " if concept is null then\n" + + " null\n" + + " else\n" + + " System.Concept {\n" + + " codes: concept.coding C return ToCode(C),\n" + + " display: concept.text.value\n" + + " }"; +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryRefreshIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryRefreshIT.java new file mode 100644 index 000000000..25704666f --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/library/LibraryRefreshIT.java @@ -0,0 +1,306 @@ +package org.opencds.cqf.tooling.operations.library; + +import ca.uhn.fhir.context.FhirContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.ModelManager; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Library; +import org.opencds.cqf.cql.evaluator.cql2elm.content.InMemoryLibrarySourceProvider; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.Date; + +public class LibraryRefreshIT { + + private final FhirContext fhirContext = FhirContext.forR4Cached(); + + @Test + void testSingleLibraryWithoutUpdate() { + ModelManager modelManager = new ModelManager(); + LibraryManager libraryManager = new LibraryManager(modelManager); + libraryManager.getLibrarySourceLoader().registerProvider( + new InMemoryLibrarySourceProvider(Collections.singletonList(AOE_CQL_UNCHANGED))); + + Library libraryToRefresh = (Library) fhirContext.newJsonParser().parseResource(AOE_LIBRARY); + + LibraryRefresh libraryRefresh = new LibraryRefresh(); + libraryRefresh.setFhirContext(fhirContext); + libraryRefresh.setModelManager(modelManager); + libraryRefresh.setLibraryManager(libraryManager); + + IBaseResource result = libraryRefresh.refreshLibrary(libraryToRefresh); + + Assert.assertTrue(result instanceof Library); + Library refreshedLibrary = (Library) result; + + // test date update + Assert.assertTrue(DateUtils.isSameDay(new Date(), refreshedLibrary.getDate())); + + // CQL content tests before update (should be the same) + Assert.assertEquals(refreshedLibrary.getContent().size(), libraryToRefresh.getContent().size()); + Assert.assertEquals(StringUtils.deleteWhitespace(new String(refreshedLibrary.getContent().get(0).getData())), + StringUtils.deleteWhitespace(new String(libraryToRefresh.getContent().get(0).getData()))); + + // DataRequirement tests before update (should be the same) + Assert.assertEquals(refreshedLibrary.getDataRequirement().size(), + libraryToRefresh.getDataRequirement().size()); + + // Parameter tests before update (should be the same) + Assert.assertEquals(refreshedLibrary.getParameter().size(), libraryToRefresh.getParameter().size()); + } + + @Test + void testSingleLibraryWithUpdate() { + ModelManager modelManager = new ModelManager(); + LibraryManager libraryManager = new LibraryManager(modelManager); + libraryManager.getLibrarySourceLoader().registerProvider( + new InMemoryLibrarySourceProvider(Collections.singletonList(AOE_CQL_UPDATED))); + + Library libraryToRefresh = (Library) fhirContext.newJsonParser().parseResource(AOE_LIBRARY); + + LibraryRefresh libraryRefresh = new LibraryRefresh(); + libraryRefresh.setFhirContext(fhirContext); + libraryRefresh.setModelManager(modelManager); + libraryRefresh.setLibraryManager(libraryManager); + + IBaseResource result = libraryRefresh.refreshLibrary(libraryToRefresh); + + Assert.assertTrue(result instanceof Library); + Library refreshedLibrary = (Library) result; + + // test date update + Assert.assertTrue(DateUtils.isSameDay(new Date(), refreshedLibrary.getDate())); + + // CQL content tests before update (should not be the same) + Assert.assertEquals(refreshedLibrary.getContent().size(), libraryToRefresh.getContent().size()); + Assert.assertNotEquals(StringUtils.deleteWhitespace(new String(refreshedLibrary.getContent().get(0).getData())), + StringUtils.deleteWhitespace(new String(libraryToRefresh.getContent().get(0).getData()))); + + // DataRequirement tests before update (should not be the same) + Assert.assertNotEquals(refreshedLibrary.getDataRequirement().size(), + libraryToRefresh.getDataRequirement().size()); + + // Parameter tests before update (should not be the same) + Assert.assertNotEquals(refreshedLibrary.getParameter().size(), libraryToRefresh.getParameter().size()); + } + + private final String AOE_CQL_UNCHANGED = "/*\n" + + "This example is a work in progress and should not be considered a final specification\n" + + "or recommendation for guidance. This example will help guide and direct the process\n" + + "of finding conventions and usage patterns that meet the needs of the various stakeholders\n" + + "in the measure development community.\n" + + "\n" + + "@update: BTR 2020-03-31 ->\n" + + "Incremented version to 2.0.000\n" + + "Updated FHIR version to 4.0.1\n" + + "*/\n" + + "library AdultOutpatientEncounters version '2.0.000'\n" + + "\n" + + "using FHIR version '4.0.1'\n" + + "\n" + + "include FHIRHelpers version '4.0.1' called FHIRHelpers\n" + + "\n" + + "valueset \"Office Visit\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001'\n" + + "valueset \"Annual Wellness Visit\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240'\n" + + "valueset \"Preventive Care Services - Established Office Visit, 18 and Up\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1025'\n" + + "valueset \"Preventive Care Services-Initial Office Visit, 18 and Up\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1023'\n" + + "valueset \"Home Healthcare Services\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1016'\n" + + "\n" + + "parameter \"Measurement Period\" Interval<DateTime>\n" + + " default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0)\n" + + "\n" + + "context Patient\n" + + "\n" + + "define \"Qualifying Encounters\":\n" + + "\t(\n" + + " [Encounter: \"Office Visit\"]\n" + + " \t\tunion [Encounter: \"Annual Wellness Visit\"]\n" + + " \t\tunion [Encounter: \"Preventive Care Services - Established Office Visit, 18 and Up\"]\n" + + " \t\tunion [Encounter: \"Preventive Care Services-Initial Office Visit, 18 and Up\"]\n" + + " \t\tunion [Encounter: \"Home Healthcare Services\"]\n" + + " ) ValidEncounter\n" + + "\t\twhere ValidEncounter.period during \"Measurement Period\"\n" + + " \t\tand ValidEncounter.status = 'finished'\n"; + + private final String AOE_CQL_UPDATED = "/*\n" + + "This example is a work in progress and should not be considered a final specification\n" + + "or recommendation for guidance. This example will help guide and direct the process\n" + + "of finding conventions and usage patterns that meet the needs of the various stakeholders\n" + + "in the measure development community.\n" + + "\n" + + "@update: BTR 2020-03-31 ->\n" + + "Incremented version to 2.0.000\n" + + "Updated FHIR version to 4.0.1\n" + + "*/\n" + + "library AdultOutpatientEncounters version '2.0.000'\n" + + "\n" + + "using FHIR version '4.0.1'\n" + + "\n" + + "include FHIRHelpers version '4.0.1' called FHIRHelpers\n" + + "\n" + + "valueset \"Office Visit\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001'\n" + + "valueset \"Annual Wellness Visit\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240'\n" + + "valueset \"Preventive Care Services - Established Office Visit, 18 and Up\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1025'\n" + + "valueset \"Preventive Care Services-Initial Office Visit, 18 and Up\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1023'\n" + + "valueset \"Home Healthcare Services\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1016'\n" + + "\n" + + "valueset \"Acute Inpatient\": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1083'\n" + + "\n" + + "parameter \"Measurement Period\" Interval<DateTime>\n" + + " default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0)\n" + + "\n" + + "context Patient\n" + + "\n" + + "define \"Qualifying Encounters\":\n" + + "\t(\n" + + " [Encounter: \"Office Visit\"]\n" + + " \t\tunion [Encounter: \"Annual Wellness Visit\"]\n" + + " \t\tunion [Encounter: \"Preventive Care Services - Established Office Visit, 18 and Up\"]\n" + + " \t\tunion [Encounter: \"Preventive Care Services-Initial Office Visit, 18 and Up\"]\n" + + " \t\tunion [Encounter: \"Home Healthcare Services\"]\n" + + " ) ValidEncounter\n" + + "\t\twhere ValidEncounter.period during \"Measurement Period\"\n" + + " \t\tand ValidEncounter.status = 'finished'\n" + + "\n" + + "define \"New Named Expression Update\":\n" + + "\t[Encounter: \"Acute Inpatient\"] AcuteInpatientEncounter\n" + + "\t\twhere AcuteInpatientEncounter.period during \"Measurement Period\"\n" + + " \t\tand AcuteInpatientEncounter.status = 'finished'"; + + private final String AOE_LIBRARY = "{\n" + + " \"resourceType\": \"Library\",\n" + + " \"id\": \"AdultOutpatientEncounters\",\n" + + " \"meta\": {\n" + + " \"profile\": [ \"http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/library-cqfm\" ]\n" + + " },\n" + + " \"url\": \"http://hl7.org/fhir/us/cqfmeasures/Library/AdultOutpatientEncounters\",\n" + + " \"identifier\": [ {\n" + + " \"use\": \"official\",\n" + + " \"system\": \"http://example.org/fhir/cqi/ecqm/Library/Identifier\",\n" + + " \"value\": \"AdultOutpatientEncounters\"\n" + + " } ],\n" + + " \"version\": \"2.0.000\",\n" + + " \"name\": \"AdultOutpatientEncounters\",\n" + + " \"title\": \"Adult Outpatient Encounters Common Library\",\n" + + " \"status\": \"active\",\n" + + " \"experimental\": true,\n" + + " \"type\": {\n" + + " \"coding\": [ {\n" + + " \"system\": \"http://terminology.hl7.org/CodeSystem/library-type\",\n" + + " \"code\": \"logic-library\"\n" + + " } ]\n" + + " },\n" + + " \"date\": \"2019-09-03\",\n" + + " \"publisher\": \"Mathematica\",\n" + + " \"description\": \"This library is used as an example in the FHIR Quality Measure Implementation Guide\",\n" + + " \"jurisdiction\": [ {\n" + + " \"coding\": [ {\n" + + " \"system\": \"urn:iso:std:iso:3166\",\n" + + " \"code\": \"US\"\n" + + " } ]\n" + + " } ],\n" + + " \"approvalDate\": \"2019-08-03\",\n" + + " \"lastReviewDate\": \"2019-08-03\",\n" + + " \"relatedArtifact\": [ {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"FHIR model information\",\n" + + " \"resource\": \"http://fhir.org/guides/cqf/common/Library/FHIR-ModelInfo|4.0.1\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Library FHIRHelpers\",\n" + + " \"resource\": \"http://fhir.org/guides/cqf/common/Library/FHIRHelpers|4.0.1\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Value set Office Visit\",\n" + + " \"resource\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Value set Annual Wellness Visit\",\n" + + " \"resource\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Value set Preventive Care Services - Established Office Visit, 18 and Up\",\n" + + " \"resource\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1025\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Value set Preventive Care Services-Initial Office Visit, 18 and Up\",\n" + + " \"resource\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1023\"\n" + + " }, {\n" + + " \"type\": \"depends-on\",\n" + + " \"display\": \"Value set Home Healthcare Services\",\n" + + " \"resource\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1016\"\n" + + " } ],\n" + + " \"parameter\": [ {\n" + + " \"name\": \"Measurement Period\",\n" + + " \"use\": \"in\",\n" + + " \"min\": 0,\n" + + " \"max\": \"1\",\n" + + " \"type\": \"Period\"\n" + + " }, {\n" + + " \"name\": \"Patient\",\n" + + " \"use\": \"out\",\n" + + " \"min\": 0,\n" + + " \"max\": \"1\",\n" + + " \"type\": \"Patient\"\n" + + " }, {\n" + + " \"name\": \"Qualifying Encounters\",\n" + + " \"use\": \"out\",\n" + + " \"min\": 0,\n" + + " \"max\": \"*\",\n" + + " \"type\": \"Encounter\"\n" + + " } ],\n" + + " \"dataRequirement\": [ {\n" + + " \"type\": \"Patient\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Patient\" ]\n" + + " }, {\n" + + " \"type\": \"Encounter\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Encounter\" ],\n" + + " \"codeFilter\": [ {\n" + + " \"path\": \"type\",\n" + + " \"valueSet\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1001\"\n" + + " } ]\n" + + " }, {\n" + + " \"type\": \"Encounter\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Encounter\" ],\n" + + " \"codeFilter\": [ {\n" + + " \"path\": \"type\",\n" + + " \"valueSet\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240\"\n" + + " } ]\n" + + " }, {\n" + + " \"type\": \"Encounter\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Encounter\" ],\n" + + " \"codeFilter\": [ {\n" + + " \"path\": \"type\",\n" + + " \"valueSet\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1025\"\n" + + " } ]\n" + + " }, {\n" + + " \"type\": \"Encounter\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Encounter\" ],\n" + + " \"codeFilter\": [ {\n" + + " \"path\": \"type\",\n" + + " \"valueSet\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1023\"\n" + + " } ]\n" + + " }, {\n" + + " \"type\": \"Encounter\",\n" + + " \"profile\": [ \"http://hl7.org/fhir/StructureDefinition/Encounter\" ],\n" + + " \"codeFilter\": [ {\n" + + " \"path\": \"type\",\n" + + " \"valueSet\": \"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.101.12.1016\"\n" + + " } ]\n" + + " } ],\n" + + " \"content\": [ {\n" + + " \"contentType\": \"text/cql\",\n" + + " \"data\": \"LyoNClRoaXMgZXhhbXBsZSBpcyBhIHdvcmsgaW4gcHJvZ3Jlc3MgYW5kIHNob3VsZCBub3QgYmUgY29uc2lkZXJlZCBhIGZpbmFsIHNwZWNpZmljYXRpb24NCm9yIHJlY29tbWVuZGF0aW9uIGZvciBndWlkYW5jZS4gVGhpcyBleGFtcGxlIHdpbGwgaGVscCBndWlkZSBhbmQgZGlyZWN0IHRoZSBwcm9jZXNzDQpvZiBmaW5kaW5nIGNvbnZlbnRpb25zIGFuZCB1c2FnZSBwYXR0ZXJucyB0aGF0IG1lZXQgdGhlIG5lZWRzIG9mIHRoZSB2YXJpb3VzIHN0YWtlaG9sZGVycw0KaW4gdGhlIG1lYXN1cmUgZGV2ZWxvcG1lbnQgY29tbXVuaXR5Lg0KDQpAdXBkYXRlOiBCVFIgMjAyMC0wMy0zMSAtPg0KSW5jcmVtZW50ZWQgdmVyc2lvbiB0byAyLjAuMDAwDQpVcGRhdGVkIEZISVIgdmVyc2lvbiB0byA0LjAuMQ0KKi8NCmxpYnJhcnkgQWR1bHRPdXRwYXRpZW50RW5jb3VudGVycyB2ZXJzaW9uICcyLjAuMDAwJw0KDQp1c2luZyBGSElSIHZlcnNpb24gJzQuMC4xJw0KDQppbmNsdWRlIEZISVJIZWxwZXJzIHZlcnNpb24gJzQuMC4xJyBjYWxsZWQgRkhJUkhlbHBlcnMNCg0KdmFsdWVzZXQgIk9mZmljZSBWaXNpdCI6ICdodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAwMScNCnZhbHVlc2V0ICJBbm51YWwgV2VsbG5lc3MgVmlzaXQiOiAnaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNTI2LjMuMTI0MCcNCnZhbHVlc2V0ICJQcmV2ZW50aXZlIENhcmUgU2VydmljZXMgLSBFc3RhYmxpc2hlZCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCI6ICdodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyNScNCnZhbHVlc2V0ICJQcmV2ZW50aXZlIENhcmUgU2VydmljZXMtSW5pdGlhbCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCI6ICdodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyMycNCnZhbHVlc2V0ICJIb21lIEhlYWx0aGNhcmUgU2VydmljZXMiOiAnaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMTYnDQoNCnBhcmFtZXRlciAiTWVhc3VyZW1lbnQgUGVyaW9kIiBJbnRlcnZhbDxEYXRlVGltZT4NCiAgZGVmYXVsdCBJbnRlcnZhbFtAMjAxOS0wMS0wMVQwMDowMDowMC4wLCBAMjAyMC0wMS0wMVQwMDowMDowMC4wKQ0KDQpjb250ZXh0IFBhdGllbnQNCg0KZGVmaW5lICJRdWFsaWZ5aW5nIEVuY291bnRlcnMiOg0KCSgNCiAgICBbRW5jb3VudGVyOiAiT2ZmaWNlIFZpc2l0Il0NCiAgCQl1bmlvbiBbRW5jb3VudGVyOiAiQW5udWFsIFdlbGxuZXNzIFZpc2l0Il0NCiAgCQl1bmlvbiBbRW5jb3VudGVyOiAiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzIC0gRXN0YWJsaXNoZWQgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiXQ0KICAJCXVuaW9uIFtFbmNvdW50ZXI6ICJQcmV2ZW50aXZlIENhcmUgU2VydmljZXMtSW5pdGlhbCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCJdDQogIAkJdW5pb24gW0VuY291bnRlcjogIkhvbWUgSGVhbHRoY2FyZSBTZXJ2aWNlcyJdDQogICkgVmFsaWRFbmNvdW50ZXINCgkJd2hlcmUgVmFsaWRFbmNvdW50ZXIucGVyaW9kIGR1cmluZyAiTWVhc3VyZW1lbnQgUGVyaW9kIg0KICAJCWFuZCBWYWxpZEVuY291bnRlci5zdGF0dXMgID0gJ2ZpbmlzaGVkJw0K\"\n" + + " }, {\n" + + " \"contentType\": \"application/elm+xml\",\n" + + " \"data\": \"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxsaWJyYXJ5IHhtbG5zPSJ1cm46aGw3LW9yZzplbG06cjEiIHhtbG5zOnQ9InVybjpobDctb3JnOmVsbS10eXBlczpyMSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6Zmhpcj0iaHR0cDovL2hsNy5vcmcvZmhpciIgeG1sbnM6cWRtNDM9InVybjpoZWFsdGhpdC1nb3Y6cWRtOnY0XzMiIHhtbG5zOnFkbTUzPSJ1cm46aGVhbHRoaXQtZ292OnFkbTp2NV8zIiB4bWxuczphPSJ1cm46aGw3LW9yZzpjcWwtYW5ub3RhdGlvbnM6cjEiPg0KICAgPGFubm90YXRpb24gdHJhbnNsYXRvck9wdGlvbnM9IkVuYWJsZUFubm90YXRpb25zLEVuYWJsZUxvY2F0b3JzLERpc2FibGVMaXN0RGVtb3Rpb24sRGlzYWJsZUxpc3RQcm9tb3Rpb24iIHhzaTp0eXBlPSJhOkNxbFRvRWxtSW5mbyIvPg0KICAgPGFubm90YXRpb24geHNpOnR5cGU9ImE6QW5ub3RhdGlvbiI+DQogICAgICA8YTp0IG5hbWU9InVwZGF0ZSIgdmFsdWU9IkJUUiAyMDIwLTAzLTMxIC0+JiN4YTtJbmNyZW1lbnRlZCB2ZXJzaW9uIHRvIDIuMC4wMDAmI3hhO1VwZGF0ZWQgRkhJUiB2ZXJzaW9uIHRvIDQuMC4xIi8+DQogICAgICA8YTpzIHI9IjM0Ij4NCiAgICAgICAgIDxhOnM+LyoKVGhpcyBleGFtcGxlIGlzIGEgd29yayBpbiBwcm9ncmVzcyBhbmQgc2hvdWxkIG5vdCBiZSBjb25zaWRlcmVkIGEgZmluYWwgc3BlY2lmaWNhdGlvbgpvciByZWNvbW1lbmRhdGlvbiBmb3IgZ3VpZGFuY2UuIFRoaXMgZXhhbXBsZSB3aWxsIGhlbHAgZ3VpZGUgYW5kIGRpcmVjdCB0aGUgcHJvY2VzcwpvZiBmaW5kaW5nIGNvbnZlbnRpb25zIGFuZCB1c2FnZSBwYXR0ZXJucyB0aGF0IG1lZXQgdGhlIG5lZWRzIG9mIHRoZSB2YXJpb3VzIHN0YWtlaG9sZGVycwppbiB0aGUgbWVhc3VyZSBkZXZlbG9wbWVudCBjb21tdW5pdHkuCgpAdXBkYXRlOiBCVFIgMjAyMC0wMy0zMSAtPgpJbmNyZW1lbnRlZCB2ZXJzaW9uIHRvIDIuMC4wMDAKVXBkYXRlZCBGSElSIHZlcnNpb24gdG8gNC4wLjEKKi9saWJyYXJ5IEFkdWx0T3V0cGF0aWVudEVuY291bnRlcnMgdmVyc2lvbiAnMi4wLjAwMCc8L2E6cz4NCiAgICAgIDwvYTpzPg0KICAgPC9hbm5vdGF0aW9uPg0KICAgPGlkZW50aWZpZXIgaWQ9IkFkdWx0T3V0cGF0aWVudEVuY291bnRlcnMiIHN5c3RlbT0iaHR0cDovL3NvbWV3aGVyZS5vcmcvZmhpci91di9teWNvbnRlbnRpZyIgdmVyc2lvbj0iMi4wLjAwMCIvPg0KICAgPHNjaGVtYUlkZW50aWZpZXIgaWQ9InVybjpobDctb3JnOmVsbSIgdmVyc2lvbj0icjEiLz4NCiAgIDx1c2luZ3M+DQogICAgICA8ZGVmIGxvY2FsSWRlbnRpZmllcj0iU3lzdGVtIiB1cmk9InVybjpobDctb3JnOmVsbS10eXBlczpyMSIvPg0KICAgICAgPGRlZiBsb2NhbElkPSIxIiBsb2NhdG9yPSIxMzoxLTEzOjI2IiBsb2NhbElkZW50aWZpZXI9IkZISVIiIHVyaT0iaHR0cDovL2hsNy5vcmcvZmhpciIgdmVyc2lvbj0iNC4wLjEiPg0KICAgICAgICAgPGFubm90YXRpb24geHNpOnR5cGU9ImE6QW5ub3RhdGlvbiI+DQogICAgICAgICAgICA8YTpzIHI9IjEiPg0KICAgICAgICAgICAgICAgPGE6cz51c2luZyA8L2E6cz4NCiAgICAgICAgICAgICAgIDxhOnM+DQogICAgICAgICAgICAgICAgICA8YTpzPkZISVI8L2E6cz4NCiAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgPGE6cz4gdmVyc2lvbiAnNC4wLjEnPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgIDwvZGVmPg0KICAgPC91c2luZ3M+DQogICA8aW5jbHVkZXM+DQogICAgICA8ZGVmIGxvY2FsSWQ9IjIiIGxvY2F0b3I9IjE1OjEtMTU6NTQiIGxvY2FsSWRlbnRpZmllcj0iRkhJUkhlbHBlcnMiIHBhdGg9Imh0dHA6Ly9maGlyLm9yZy9ndWlkZXMvY3FmL2NvbW1vbi9GSElSSGVscGVycyIgdmVyc2lvbj0iNC4wLjEiPg0KICAgICAgICAgPGFubm90YXRpb24geHNpOnR5cGU9ImE6QW5ub3RhdGlvbiI+DQogICAgICAgICAgICA8YTpzIHI9IjIiPg0KICAgICAgICAgICAgICAgPGE6cz5pbmNsdWRlIDwvYTpzPg0KICAgICAgICAgICAgICAgPGE6cz4NCiAgICAgICAgICAgICAgICAgIDxhOnM+RkhJUkhlbHBlcnM8L2E6cz4NCiAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgPGE6cz4gdmVyc2lvbiAnNC4wLjEnIGNhbGxlZCBGSElSSGVscGVyczwvYTpzPg0KICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICA8L2Fubm90YXRpb24+DQogICAgICA8L2RlZj4NCiAgIDwvaW5jbHVkZXM+DQogICA8cGFyYW1ldGVycz4NCiAgICAgIDxkZWYgbG9jYWxJZD0iMTMiIGxvY2F0b3I9IjIzOjEtMjQ6NjYiIG5hbWU9Ik1lYXN1cmVtZW50IFBlcmlvZCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+DQogICAgICAgICA8YW5ub3RhdGlvbiB4c2k6dHlwZT0iYTpBbm5vdGF0aW9uIj4NCiAgICAgICAgICAgIDxhOnMgcj0iMTMiPg0KICAgICAgICAgICAgICAgPGE6cz5wYXJhbWV0ZXIgJnF1b3Q7TWVhc3VyZW1lbnQgUGVyaW9kJnF1b3Q7IDwvYTpzPg0KICAgICAgICAgICAgICAgPGE6cyByPSIxMiI+DQogICAgICAgICAgICAgICAgICA8YTpzPkludGVydmFsJmx0OzwvYTpzPg0KICAgICAgICAgICAgICAgICAgPGE6cyByPSIxMSI+DQogICAgICAgICAgICAgICAgICAgICA8YTpzPkRhdGVUaW1lPC9hOnM+DQogICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgIDxhOnM+PjwvYTpzPg0KICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICA8YTpzPgogIGRlZmF1bHQgPC9hOnM+DQogICAgICAgICAgICAgICA8YTpzIHI9IjEwIj4NCiAgICAgICAgICAgICAgICAgIDxhOnMgcj0iOCI+SW50ZXJ2YWxbQDIwMTktMDEtMDFUMDA6MDA6MDAuMCwgQDIwMjAtMDEtMDFUMDA6MDA6MDAuMCk8L2E6cz4NCiAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICA8L2Fubm90YXRpb24+DQogICAgICAgICA8ZGVmYXVsdCBsb2NhbElkPSIxMCIgbG9jYXRvcj0iMjQ6MTEtMjQ6NjYiIGxvd0Nsb3NlZD0idHJ1ZSIgaGlnaENsb3NlZD0iZmFsc2UiIHhzaTp0eXBlPSJJbnRlcnZhbCI+DQogICAgICAgICAgICA8bG93IGxvY2FsSWQ9IjgiIGxvY2F0b3I9IjI0OjIwLTI0OjQxIiB4c2k6dHlwZT0iRGF0ZVRpbWUiPg0KICAgICAgICAgICAgICAgPHllYXIgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIyMDE5IiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPG1vbnRoIHZhbHVlVHlwZT0idDpJbnRlZ2VyIiB2YWx1ZT0iMSIgeHNpOnR5cGU9IkxpdGVyYWwiLz4NCiAgICAgICAgICAgICAgIDxkYXkgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIxIiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPGhvdXIgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIwIiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPG1pbnV0ZSB2YWx1ZVR5cGU9InQ6SW50ZWdlciIgdmFsdWU9IjAiIHhzaTp0eXBlPSJMaXRlcmFsIi8+DQogICAgICAgICAgICAgICA8c2Vjb25kIHZhbHVlVHlwZT0idDpJbnRlZ2VyIiB2YWx1ZT0iMCIgeHNpOnR5cGU9IkxpdGVyYWwiLz4NCiAgICAgICAgICAgICAgIDxtaWxsaXNlY29uZCB2YWx1ZVR5cGU9InQ6SW50ZWdlciIgdmFsdWU9IjAiIHhzaTp0eXBlPSJMaXRlcmFsIi8+DQogICAgICAgICAgICA8L2xvdz4NCiAgICAgICAgICAgIDxoaWdoIGxvY2FsSWQ9IjkiIGxvY2F0b3I9IjI0OjQ0LTI0OjY1IiB4c2k6dHlwZT0iRGF0ZVRpbWUiPg0KICAgICAgICAgICAgICAgPHllYXIgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIyMDIwIiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPG1vbnRoIHZhbHVlVHlwZT0idDpJbnRlZ2VyIiB2YWx1ZT0iMSIgeHNpOnR5cGU9IkxpdGVyYWwiLz4NCiAgICAgICAgICAgICAgIDxkYXkgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIxIiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPGhvdXIgdmFsdWVUeXBlPSJ0OkludGVnZXIiIHZhbHVlPSIwIiB4c2k6dHlwZT0iTGl0ZXJhbCIvPg0KICAgICAgICAgICAgICAgPG1pbnV0ZSB2YWx1ZVR5cGU9InQ6SW50ZWdlciIgdmFsdWU9IjAiIHhzaTp0eXBlPSJMaXRlcmFsIi8+DQogICAgICAgICAgICAgICA8c2Vjb25kIHZhbHVlVHlwZT0idDpJbnRlZ2VyIiB2YWx1ZT0iMCIgeHNpOnR5cGU9IkxpdGVyYWwiLz4NCiAgICAgICAgICAgICAgIDxtaWxsaXNlY29uZCB2YWx1ZVR5cGU9InQ6SW50ZWdlciIgdmFsdWU9IjAiIHhzaTp0eXBlPSJMaXRlcmFsIi8+DQogICAgICAgICAgICA8L2hpZ2g+DQogICAgICAgICA8L2RlZmF1bHQ+DQogICAgICAgICA8cGFyYW1ldGVyVHlwZVNwZWNpZmllciBsb2NhbElkPSIxMiIgbG9jYXRvcj0iMjM6MzItMjM6NDkiIHhzaTp0eXBlPSJJbnRlcnZhbFR5cGVTcGVjaWZpZXIiPg0KICAgICAgICAgICAgPHBvaW50VHlwZSBsb2NhbElkPSIxMSIgbG9jYXRvcj0iMjM6NDEtMjM6NDgiIG5hbWU9InQ6RGF0ZVRpbWUiIHhzaTp0eXBlPSJOYW1lZFR5cGVTcGVjaWZpZXIiLz4NCiAgICAgICAgIDwvcGFyYW1ldGVyVHlwZVNwZWNpZmllcj4NCiAgICAgIDwvZGVmPg0KICAgPC9wYXJhbWV0ZXJzPg0KICAgPHZhbHVlU2V0cz4NCiAgICAgIDxkZWYgbG9jYWxJZD0iMyIgbG9jYXRvcj0iMTc6MS0xNzoxMDQiIG5hbWU9Ik9mZmljZSBWaXNpdCIgaWQ9Imh0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjQ2NC4xMDAzLjEwMS4xMi4xMDAxIiBhY2Nlc3NMZXZlbD0iUHVibGljIj4NCiAgICAgICAgIDxhbm5vdGF0aW9uIHhzaTp0eXBlPSJhOkFubm90YXRpb24iPg0KICAgICAgICAgICAgPGE6cyByPSIzIj4NCiAgICAgICAgICAgICAgIDxhOnM+dmFsdWVzZXQgJnF1b3Q7T2ZmaWNlIFZpc2l0JnF1b3Q7OiAnaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMDEnPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgIDwvZGVmPg0KICAgICAgPGRlZiBsb2NhbElkPSI0IiBsb2NhdG9yPSIxODoxLTE4OjEwMyIgbmFtZT0iQW5udWFsIFdlbGxuZXNzIFZpc2l0IiBpZD0iaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNTI2LjMuMTI0MCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+DQogICAgICAgICA8YW5ub3RhdGlvbiB4c2k6dHlwZT0iYTpBbm5vdGF0aW9uIj4NCiAgICAgICAgICAgIDxhOnMgcj0iNCI+DQogICAgICAgICAgICAgICA8YTpzPnZhbHVlc2V0ICZxdW90O0FubnVhbCBXZWxsbmVzcyBWaXNpdCZxdW90OzogJ2h0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjUyNi4zLjEyNDAnPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgIDwvZGVmPg0KICAgICAgPGRlZiBsb2NhbElkPSI1IiBsb2NhdG9yPSIxOToxLTE5OjE1NCIgbmFtZT0iUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzIC0gRXN0YWJsaXNoZWQgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiIGlkPSJodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyNSIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+DQogICAgICAgICA8YW5ub3RhdGlvbiB4c2k6dHlwZT0iYTpBbm5vdGF0aW9uIj4NCiAgICAgICAgICAgIDxhOnMgcj0iNSI+DQogICAgICAgICAgICAgICA8YTpzPnZhbHVlc2V0ICZxdW90O1ByZXZlbnRpdmUgQ2FyZSBTZXJ2aWNlcyAtIEVzdGFibGlzaGVkIE9mZmljZSBWaXNpdCwgMTggYW5kIFVwJnF1b3Q7OiAnaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMjUnPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgIDwvZGVmPg0KICAgICAgPGRlZiBsb2NhbElkPSI2IiBsb2NhdG9yPSIyMDoxLTIwOjE0OCIgbmFtZT0iUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzLUluaXRpYWwgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiIGlkPSJodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyMyIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+DQogICAgICAgICA8YW5ub3RhdGlvbiB4c2k6dHlwZT0iYTpBbm5vdGF0aW9uIj4NCiAgICAgICAgICAgIDxhOnMgcj0iNiI+DQogICAgICAgICAgICAgICA8YTpzPnZhbHVlc2V0ICZxdW90O1ByZXZlbnRpdmUgQ2FyZSBTZXJ2aWNlcy1Jbml0aWFsIE9mZmljZSBWaXNpdCwgMTggYW5kIFVwJnF1b3Q7OiAnaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMjMnPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgIDwvZGVmPg0KICAgICAgPGRlZiBsb2NhbElkPSI3IiBsb2NhdG9yPSIyMToxLTIxOjExNiIgbmFtZT0iSG9tZSBIZWFsdGhjYXJlIFNlcnZpY2VzIiBpZD0iaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMTYiIGFjY2Vzc0xldmVsPSJQdWJsaWMiPg0KICAgICAgICAgPGFubm90YXRpb24geHNpOnR5cGU9ImE6QW5ub3RhdGlvbiI+DQogICAgICAgICAgICA8YTpzIHI9IjciPg0KICAgICAgICAgICAgICAgPGE6cz52YWx1ZXNldCAmcXVvdDtIb21lIEhlYWx0aGNhcmUgU2VydmljZXMmcXVvdDs6ICdodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAxNic8L2E6cz4NCiAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgPC9hbm5vdGF0aW9uPg0KICAgICAgPC9kZWY+DQogICA8L3ZhbHVlU2V0cz4NCiAgIDxjb250ZXh0cz4NCiAgICAgIDxkZWYgbG9jYXRvcj0iMjY6MS0yNjoxNSIgbmFtZT0iUGF0aWVudCIvPg0KICAgPC9jb250ZXh0cz4NCiAgIDxzdGF0ZW1lbnRzPg0KICAgICAgPGRlZiBsb2NhdG9yPSIyNjoxLTI2OjE1IiBuYW1lPSJQYXRpZW50IiBjb250ZXh0PSJQYXRpZW50Ij4NCiAgICAgICAgIDxleHByZXNzaW9uIHhzaTp0eXBlPSJTaW5nbGV0b25Gcm9tIj4NCiAgICAgICAgICAgIDxvcGVyYW5kIGxvY2F0b3I9IjI2OjEtMjY6MTUiIGRhdGFUeXBlPSJmaGlyOlBhdGllbnQiIHRlbXBsYXRlSWQ9Imh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9QYXRpZW50IiB4c2k6dHlwZT0iUmV0cmlldmUiLz4NCiAgICAgICAgIDwvZXhwcmVzc2lvbj4NCiAgICAgIDwvZGVmPg0KICAgICAgPGRlZiBsb2NhbElkPSIzNCIgbG9jYXRvcj0iMjg6MS0zNzo0MyIgbmFtZT0iUXVhbGlmeWluZyBFbmNvdW50ZXJzIiBjb250ZXh0PSJQYXRpZW50IiBhY2Nlc3NMZXZlbD0iUHVibGljIj4NCiAgICAgICAgIDxhbm5vdGF0aW9uIHhzaTp0eXBlPSJhOkFubm90YXRpb24iPg0KICAgICAgICAgICAgPGE6cyByPSIzNCI+DQogICAgICAgICAgICAgICA8YTpzPmRlZmluZSAmcXVvdDtRdWFsaWZ5aW5nIEVuY291bnRlcnMmcXVvdDs6Cgk8L2E6cz4NCiAgICAgICAgICAgICAgIDxhOnMgcj0iMzMiPg0KICAgICAgICAgICAgICAgICAgPGE6cz4NCiAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMjMiPg0KICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyMiI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPigKICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyMiI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjIwIj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMTgiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIxNiI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjE0Ij4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+W0VuY291bnRlcjogPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz4mcXVvdDtPZmZpY2UgVmlzaXQmcXVvdDs8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz5dPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+CiAgCQl1bmlvbiA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMTUiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz5bRW5jb3VudGVyOiA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPiZxdW90O0FubnVhbCBXZWxsbmVzcyBWaXNpdCZxdW90OzwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPl08L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPgogIAkJdW5pb24gPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjE3Ij4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+W0VuY291bnRlcjogPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz4mcXVvdDtQcmV2ZW50aXZlIENhcmUgU2VydmljZXMgLSBFc3RhYmxpc2hlZCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCZxdW90OzwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPl08L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPgogIAkJdW5pb24gPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjE5Ij4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+W0VuY291bnRlcjogPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz4mcXVvdDtQcmV2ZW50aXZlIENhcmUgU2VydmljZXMtSW5pdGlhbCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCZxdW90OzwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPl08L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPgogIAkJdW5pb24gPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjIxIj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+W0VuY291bnRlcjogPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz4mcXVvdDtIb21lIEhlYWx0aGNhcmUgU2VydmljZXMmcXVvdDs8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz5dPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz4KICApPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+IFZhbGlkRW5jb3VudGVyPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgPGE6cz4KCQk8L2E6cz4NCiAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMzIiPg0KICAgICAgICAgICAgICAgICAgICAgPGE6cz53aGVyZSA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMzIiPg0KICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyNyI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjI1Ij4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMjQiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz5WYWxpZEVuY291bnRlcjwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPi48L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMjUiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cz5wZXJpb2Q8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjI3Ij4gZHVyaW5nIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyNiI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPiZxdW90O01lYXN1cmVtZW50IFBlcmlvZCZxdW90OzwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+CiAgCQlhbmQgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICA8YTpzIHI9IjMxIj4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnMgcj0iMjkiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyOCI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPlZhbGlkRW5jb3VudGVyPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+LjwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIyOSI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPnN0YXR1czwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIDxhOnM+ICA9IDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPGE6cyByPSIzMCI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YTpzPidmaW5pc2hlZCc8L2E6cz4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgICAgICAgICAgIDwvYTpzPg0KICAgICAgICAgICAgICAgPC9hOnM+DQogICAgICAgICAgICA8L2E6cz4NCiAgICAgICAgIDwvYW5ub3RhdGlvbj4NCiAgICAgICAgIDxleHByZXNzaW9uIGxvY2FsSWQ9IjMzIiBsb2NhdG9yPSIyOToyLTM3OjQzIiB4c2k6dHlwZT0iUXVlcnkiPg0KICAgICAgICAgICAgPHNvdXJjZSBsb2NhbElkPSIyMyIgbG9jYXRvcj0iMjk6Mi0zNToxOCIgYWxpYXM9IlZhbGlkRW5jb3VudGVyIj4NCiAgICAgICAgICAgICAgIDxleHByZXNzaW9uIGxvY2FsSWQ9IjIyIiBsb2NhdG9yPSIyOToyLTM1OjMiIHhzaTp0eXBlPSJVbmlvbiI+DQogICAgICAgICAgICAgICAgICA8b3BlcmFuZCBsb2NhbElkPSIyMCIgbG9jYXRvcj0iMzA6NS0zMzo4MSIgeHNpOnR5cGU9IlVuaW9uIj4NCiAgICAgICAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2FsSWQ9IjE2IiBsb2NhdG9yPSIzMDo1LTMxOjQ2IiB4c2k6dHlwZT0iVW5pb24iPg0KICAgICAgICAgICAgICAgICAgICAgICAgPG9wZXJhbmQgbG9jYWxJZD0iMTQiIGxvY2F0b3I9IjMwOjUtMzA6MzEiIGRhdGFUeXBlPSJmaGlyOkVuY291bnRlciIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0VuY291bnRlciIgY29kZVByb3BlcnR5PSJ0eXBlIiBjb2RlQ29tcGFyYXRvcj0iaW4iIHhzaTp0eXBlPSJSZXRyaWV2ZSI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8Y29kZXMgbG9jYXRvcj0iMzA6MTctMzA6MzAiIG5hbWU9Ik9mZmljZSBWaXNpdCIgeHNpOnR5cGU9IlZhbHVlU2V0UmVmIi8+DQogICAgICAgICAgICAgICAgICAgICAgICA8L29wZXJhbmQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8b3BlcmFuZCBsb2NhbElkPSIxNSIgbG9jYXRvcj0iMzE6MTEtMzE6NDYiIGRhdGFUeXBlPSJmaGlyOkVuY291bnRlciIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0VuY291bnRlciIgY29kZVByb3BlcnR5PSJ0eXBlIiBjb2RlQ29tcGFyYXRvcj0iaW4iIHhzaTp0eXBlPSJSZXRyaWV2ZSI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8Y29kZXMgbG9jYXRvcj0iMzE6MjMtMzE6NDUiIG5hbWU9IkFubnVhbCBXZWxsbmVzcyBWaXNpdCIgeHNpOnR5cGU9IlZhbHVlU2V0UmVmIi8+DQogICAgICAgICAgICAgICAgICAgICAgICA8L29wZXJhbmQ+DQogICAgICAgICAgICAgICAgICAgICA8L29wZXJhbmQ+DQogICAgICAgICAgICAgICAgICAgICA8b3BlcmFuZCB4c2k6dHlwZT0iVW5pb24iPg0KICAgICAgICAgICAgICAgICAgICAgICAgPG9wZXJhbmQgbG9jYWxJZD0iMTciIGxvY2F0b3I9IjMyOjExLTMyOjg3IiBkYXRhVHlwZT0iZmhpcjpFbmNvdW50ZXIiIHRlbXBsYXRlSWQ9Imh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9FbmNvdW50ZXIiIGNvZGVQcm9wZXJ0eT0idHlwZSIgY29kZUNvbXBhcmF0b3I9ImluIiB4c2k6dHlwZT0iUmV0cmlldmUiPg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgPGNvZGVzIGxvY2F0b3I9IjMyOjIzLTMyOjg2IiBuYW1lPSJQcmV2ZW50aXZlIENhcmUgU2VydmljZXMgLSBFc3RhYmxpc2hlZCBPZmZpY2UgVmlzaXQsIDE4IGFuZCBVcCIgeHNpOnR5cGU9IlZhbHVlU2V0UmVmIi8+DQogICAgICAgICAgICAgICAgICAgICAgICA8L29wZXJhbmQ+DQogICAgICAgICAgICAgICAgICAgICAgICA8b3BlcmFuZCBsb2NhbElkPSIxOSIgbG9jYXRvcj0iMzM6MTEtMzM6ODEiIGRhdGFUeXBlPSJmaGlyOkVuY291bnRlciIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0VuY291bnRlciIgY29kZVByb3BlcnR5PSJ0eXBlIiBjb2RlQ29tcGFyYXRvcj0iaW4iIHhzaTp0eXBlPSJSZXRyaWV2ZSI+DQogICAgICAgICAgICAgICAgICAgICAgICAgICA8Y29kZXMgbG9jYXRvcj0iMzM6MjMtMzM6ODAiIG5hbWU9IlByZXZlbnRpdmUgQ2FyZSBTZXJ2aWNlcy1Jbml0aWFsIE9mZmljZSBWaXNpdCwgMTggYW5kIFVwIiB4c2k6dHlwZT0iVmFsdWVTZXRSZWYiLz4NCiAgICAgICAgICAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2FsSWQ9IjIxIiBsb2NhdG9yPSIzNDoxMS0zNDo0OSIgZGF0YVR5cGU9ImZoaXI6RW5jb3VudGVyIiB0ZW1wbGF0ZUlkPSJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vRW5jb3VudGVyIiBjb2RlUHJvcGVydHk9InR5cGUiIGNvZGVDb21wYXJhdG9yPSJpbiIgeHNpOnR5cGU9IlJldHJpZXZlIj4NCiAgICAgICAgICAgICAgICAgICAgIDxjb2RlcyBsb2NhdG9yPSIzNDoyMy0zNDo0OCIgbmFtZT0iSG9tZSBIZWFsdGhjYXJlIFNlcnZpY2VzIiB4c2k6dHlwZT0iVmFsdWVTZXRSZWYiLz4NCiAgICAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgICAgIDwvZXhwcmVzc2lvbj4NCiAgICAgICAgICAgIDwvc291cmNlPg0KICAgICAgICAgICAgPHdoZXJlIGxvY2FsSWQ9IjMyIiBsb2NhdG9yPSIzNjozLTM3OjQzIiB4c2k6dHlwZT0iQW5kIj4NCiAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2FsSWQ9IjI3IiBsb2NhdG9yPSIzNjo5LTM2OjU3IiB4c2k6dHlwZT0iSW5jbHVkZWRJbiI+DQogICAgICAgICAgICAgICAgICA8b3BlcmFuZCBuYW1lPSJUb0ludGVydmFsIiBsaWJyYXJ5TmFtZT0iRkhJUkhlbHBlcnMiIHhzaTp0eXBlPSJGdW5jdGlvblJlZiI+DQogICAgICAgICAgICAgICAgICAgICA8b3BlcmFuZCBsb2NhbElkPSIyNSIgbG9jYXRvcj0iMzY6OS0zNjoyOSIgcGF0aD0icGVyaW9kIiBzY29wZT0iVmFsaWRFbmNvdW50ZXIiIHhzaTp0eXBlPSJQcm9wZXJ0eSIvPg0KICAgICAgICAgICAgICAgICAgPC9vcGVyYW5kPg0KICAgICAgICAgICAgICAgICAgPG9wZXJhbmQgbG9jYWxJZD0iMjYiIGxvY2F0b3I9IjM2OjM4LTM2OjU3IiBuYW1lPSJNZWFzdXJlbWVudCBQZXJpb2QiIHhzaTp0eXBlPSJQYXJhbWV0ZXJSZWYiLz4NCiAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2FsSWQ9IjMxIiBsb2NhdG9yPSIzNzo5LTM3OjQzIiB4c2k6dHlwZT0iRXF1YWwiPg0KICAgICAgICAgICAgICAgICAgPG9wZXJhbmQgbmFtZT0iVG9TdHJpbmciIGxpYnJhcnlOYW1lPSJGSElSSGVscGVycyIgeHNpOnR5cGU9IkZ1bmN0aW9uUmVmIj4NCiAgICAgICAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2FsSWQ9IjI5IiBsb2NhdG9yPSIzNzo5LTM3OjI5IiBwYXRoPSJzdGF0dXMiIHNjb3BlPSJWYWxpZEVuY291bnRlciIgeHNpOnR5cGU9IlByb3BlcnR5Ii8+DQogICAgICAgICAgICAgICAgICA8L29wZXJhbmQ+DQogICAgICAgICAgICAgICAgICA8b3BlcmFuZCBsb2NhbElkPSIzMCIgbG9jYXRvcj0iMzc6MzQtMzc6NDMiIHZhbHVlVHlwZT0idDpTdHJpbmciIHZhbHVlPSJmaW5pc2hlZCIgeHNpOnR5cGU9IkxpdGVyYWwiLz4NCiAgICAgICAgICAgICAgIDwvb3BlcmFuZD4NCiAgICAgICAgICAgIDwvd2hlcmU+DQogICAgICAgICA8L2V4cHJlc3Npb24+DQogICAgICA8L2RlZj4NCiAgIDwvc3RhdGVtZW50cz4NCjwvbGlicmFyeT4NCg==\"\n" + + " }, {\n" + + " \"contentType\": \"application/elm+json\",\n" + + " \"data\": \"ew0KICAgImxpYnJhcnkiIDogew0KICAgICAgImFubm90YXRpb24iIDogWyB7DQogICAgICAgICAidHJhbnNsYXRvck9wdGlvbnMiIDogIkVuYWJsZUFubm90YXRpb25zLEVuYWJsZUxvY2F0b3JzLERpc2FibGVMaXN0RGVtb3Rpb24sRGlzYWJsZUxpc3RQcm9tb3Rpb24iLA0KICAgICAgICAgInR5cGUiIDogIkNxbFRvRWxtSW5mbyINCiAgICAgIH0sIHsNCiAgICAgICAgICJ0eXBlIiA6ICJBbm5vdGF0aW9uIiwNCiAgICAgICAgICJ0IiA6IFsgew0KICAgICAgICAgICAgIm5hbWUiIDogInVwZGF0ZSIsDQogICAgICAgICAgICAidmFsdWUiIDogIkJUUiAyMDIwLTAzLTMxIC0+XG5JbmNyZW1lbnRlZCB2ZXJzaW9uIHRvIDIuMC4wMDBcblVwZGF0ZWQgRkhJUiB2ZXJzaW9uIHRvIDQuMC4xIg0KICAgICAgICAgfSBdLA0KICAgICAgICAgInMiIDogew0KICAgICAgICAgICAgInIiIDogIjM0IiwNCiAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIi8qXG5UaGlzIGV4YW1wbGUgaXMgYSB3b3JrIGluIHByb2dyZXNzIGFuZCBzaG91bGQgbm90IGJlIGNvbnNpZGVyZWQgYSBmaW5hbCBzcGVjaWZpY2F0aW9uXG5vciByZWNvbW1lbmRhdGlvbiBmb3IgZ3VpZGFuY2UuIFRoaXMgZXhhbXBsZSB3aWxsIGhlbHAgZ3VpZGUgYW5kIGRpcmVjdCB0aGUgcHJvY2Vzc1xub2YgZmluZGluZyBjb252ZW50aW9ucyBhbmQgdXNhZ2UgcGF0dGVybnMgdGhhdCBtZWV0IHRoZSBuZWVkcyBvZiB0aGUgdmFyaW91cyBzdGFrZWhvbGRlcnNcbmluIHRoZSBtZWFzdXJlIGRldmVsb3BtZW50IGNvbW11bml0eS5cblxuQHVwZGF0ZTogQlRSIDIwMjAtMDMtMzEgLT5cbkluY3JlbWVudGVkIHZlcnNpb24gdG8gMi4wLjAwMFxuVXBkYXRlZCBGSElSIHZlcnNpb24gdG8gNC4wLjFcbiovIiwibGlicmFyeSBBZHVsdE91dHBhdGllbnRFbmNvdW50ZXJzIHZlcnNpb24gJzIuMC4wMDAnIiBdDQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0NCiAgICAgIH0gXSwNCiAgICAgICJpZGVudGlmaWVyIiA6IHsNCiAgICAgICAgICJpZCIgOiAiQWR1bHRPdXRwYXRpZW50RW5jb3VudGVycyIsDQogICAgICAgICAic3lzdGVtIiA6ICJodHRwOi8vc29tZXdoZXJlLm9yZy9maGlyL3V2L215Y29udGVudGlnIiwNCiAgICAgICAgICJ2ZXJzaW9uIiA6ICIyLjAuMDAwIg0KICAgICAgfSwNCiAgICAgICJzY2hlbWFJZGVudGlmaWVyIiA6IHsNCiAgICAgICAgICJpZCIgOiAidXJuOmhsNy1vcmc6ZWxtIiwNCiAgICAgICAgICJ2ZXJzaW9uIiA6ICJyMSINCiAgICAgIH0sDQogICAgICAidXNpbmdzIiA6IHsNCiAgICAgICAgICJkZWYiIDogWyB7DQogICAgICAgICAgICAibG9jYWxJZGVudGlmaWVyIiA6ICJTeXN0ZW0iLA0KICAgICAgICAgICAgInVyaSIgOiAidXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxIg0KICAgICAgICAgfSwgew0KICAgICAgICAgICAgImxvY2FsSWQiIDogIjEiLA0KICAgICAgICAgICAgImxvY2F0b3IiIDogIjEzOjEtMTM6MjYiLA0KICAgICAgICAgICAgImxvY2FsSWRlbnRpZmllciIgOiAiRkhJUiIsDQogICAgICAgICAgICAidXJpIiA6ICJodHRwOi8vaGw3Lm9yZy9maGlyIiwNCiAgICAgICAgICAgICJ2ZXJzaW9uIiA6ICI0LjAuMSIsDQogICAgICAgICAgICAiYW5ub3RhdGlvbiIgOiBbIHsNCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJBbm5vdGF0aW9uIiwNCiAgICAgICAgICAgICAgICJzIiA6IHsNCiAgICAgICAgICAgICAgICAgICJyIiA6ICIxIiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsInVzaW5nICIgXQ0KICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiRkhJUiIgXQ0KICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiIHZlcnNpb24gIiwiJzQuMC4xJyIgXQ0KICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0gXQ0KICAgICAgfSwNCiAgICAgICJpbmNsdWRlcyIgOiB7DQogICAgICAgICAiZGVmIiA6IFsgew0KICAgICAgICAgICAgImxvY2FsSWQiIDogIjIiLA0KICAgICAgICAgICAgImxvY2F0b3IiIDogIjE1OjEtMTU6NTQiLA0KICAgICAgICAgICAgImxvY2FsSWRlbnRpZmllciIgOiAiRkhJUkhlbHBlcnMiLA0KICAgICAgICAgICAgInBhdGgiIDogImh0dHA6Ly9maGlyLm9yZy9ndWlkZXMvY3FmL2NvbW1vbi9GSElSSGVscGVycyIsDQogICAgICAgICAgICAidmVyc2lvbiIgOiAiNC4wLjEiLA0KICAgICAgICAgICAgImFubm90YXRpb24iIDogWyB7DQogICAgICAgICAgICAgICAidHlwZSIgOiAiQW5ub3RhdGlvbiIsDQogICAgICAgICAgICAgICAicyIgOiB7DQogICAgICAgICAgICAgICAgICAiciIgOiAiMiIsDQogICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICIiLCJpbmNsdWRlICIgXQ0KICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiRkhJUkhlbHBlcnMiIF0NCiAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiB2ZXJzaW9uICIsIic0LjAuMSciLCIgY2FsbGVkICIsIkZISVJIZWxwZXJzIiBdDQogICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0gXQ0KICAgICAgICAgfSBdDQogICAgICB9LA0KICAgICAgInBhcmFtZXRlcnMiIDogew0KICAgICAgICAgImRlZiIgOiBbIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICIxMyIsDQogICAgICAgICAgICAibG9jYXRvciIgOiAiMjM6MS0yNDo2NiIsDQogICAgICAgICAgICAibmFtZSIgOiAiTWVhc3VyZW1lbnQgUGVyaW9kIiwNCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwNCiAgICAgICAgICAgICJhbm5vdGF0aW9uIiA6IFsgew0KICAgICAgICAgICAgICAgInR5cGUiIDogIkFubm90YXRpb24iLA0KICAgICAgICAgICAgICAgInMiIDogew0KICAgICAgICAgICAgICAgICAgInIiIDogIjEzIiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsInBhcmFtZXRlciAiLCJcIk1lYXN1cmVtZW50IFBlcmlvZFwiIiwiICIgXQ0KICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgInIiIDogIjEyIiwNCiAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIkludGVydmFsPCIgXQ0KICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjExIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIkRhdGVUaW1lIiBdDQogICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICI+IiBdDQogICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJcbiAgZGVmYXVsdCAiIF0NCiAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIxMCIsDQogICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICI4IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJJbnRlcnZhbFsiLCJAMjAxOS0wMS0wMVQwMDowMDowMC4wIiwiLCAiLCJAMjAyMC0wMS0wMVQwMDowMDowMC4wIiwiKSIgXQ0KICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0gXSwNCiAgICAgICAgICAgICJkZWZhdWx0IiA6IHsNCiAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIxMCIsDQogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjQ6MTEtMjQ6NjYiLA0KICAgICAgICAgICAgICAgImxvd0Nsb3NlZCIgOiB0cnVlLA0KICAgICAgICAgICAgICAgImhpZ2hDbG9zZWQiIDogZmFsc2UsDQogICAgICAgICAgICAgICAidHlwZSIgOiAiSW50ZXJ2YWwiLA0KICAgICAgICAgICAgICAgImxvdyIgOiB7DQogICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiOCIsDQogICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjQ6MjAtMjQ6NDEiLA0KICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkRhdGVUaW1lIiwNCiAgICAgICAgICAgICAgICAgICJ5ZWFyIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjIwMTkiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgIm1vbnRoIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjEiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgImRheSIgOiB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWVUeXBlIiA6ICJ7dXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxfUludGVnZXIiLA0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6ICIxIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJMaXRlcmFsIg0KICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICJob3VyIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjAiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgIm1pbnV0ZSIgOiB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWVUeXBlIiA6ICJ7dXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxfUludGVnZXIiLA0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6ICIwIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJMaXRlcmFsIg0KICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICJzZWNvbmQiIDogew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlVHlwZSIgOiAie3VybjpobDctb3JnOmVsbS10eXBlczpyMX1JbnRlZ2VyIiwNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiAiMCIsDQogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiTGl0ZXJhbCINCiAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAibWlsbGlzZWNvbmQiIDogew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlVHlwZSIgOiAie3VybjpobDctb3JnOmVsbS10eXBlczpyMX1JbnRlZ2VyIiwNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiAiMCIsDQogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiTGl0ZXJhbCINCiAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAiaGlnaCIgOiB7DQogICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiOSIsDQogICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjQ6NDQtMjQ6NjUiLA0KICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkRhdGVUaW1lIiwNCiAgICAgICAgICAgICAgICAgICJ5ZWFyIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjIwMjAiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgIm1vbnRoIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjEiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgImRheSIgOiB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWVUeXBlIiA6ICJ7dXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxfUludGVnZXIiLA0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6ICIxIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJMaXRlcmFsIg0KICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICJob3VyIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9SW50ZWdlciIsDQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogIjAiLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgICAgIm1pbnV0ZSIgOiB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWVUeXBlIiA6ICJ7dXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxfUludGVnZXIiLA0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6ICIwIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJMaXRlcmFsIg0KICAgICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICAgICJzZWNvbmQiIDogew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlVHlwZSIgOiAie3VybjpobDctb3JnOmVsbS10eXBlczpyMX1JbnRlZ2VyIiwNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiAiMCIsDQogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiTGl0ZXJhbCINCiAgICAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAgICAibWlsbGlzZWNvbmQiIDogew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlVHlwZSIgOiAie3VybjpobDctb3JnOmVsbS10eXBlczpyMX1JbnRlZ2VyIiwNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiAiMCIsDQogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiTGl0ZXJhbCINCiAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICAicGFyYW1ldGVyVHlwZVNwZWNpZmllciIgOiB7DQogICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMTIiLA0KICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjIzOjMyLTIzOjQ5IiwNCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJJbnRlcnZhbFR5cGVTcGVjaWZpZXIiLA0KICAgICAgICAgICAgICAgInBvaW50VHlwZSIgOiB7DQogICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMTEiLA0KICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjIzOjQxLTIzOjQ4IiwNCiAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJ7dXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxfURhdGVUaW1lIiwNCiAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJOYW1lZFR5cGVTcGVjaWZpZXIiDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICB9IF0NCiAgICAgIH0sDQogICAgICAidmFsdWVTZXRzIiA6IHsNCiAgICAgICAgICJkZWYiIDogWyB7DQogICAgICAgICAgICAibG9jYWxJZCIgOiAiMyIsDQogICAgICAgICAgICAibG9jYXRvciIgOiAiMTc6MS0xNzoxMDQiLA0KICAgICAgICAgICAgIm5hbWUiIDogIk9mZmljZSBWaXNpdCIsDQogICAgICAgICAgICAiaWQiIDogImh0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjQ2NC4xMDAzLjEwMS4xMi4xMDAxIiwNCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwNCiAgICAgICAgICAgICJhbm5vdGF0aW9uIiA6IFsgew0KICAgICAgICAgICAgICAgInR5cGUiIDogIkFubm90YXRpb24iLA0KICAgICAgICAgICAgICAgInMiIDogew0KICAgICAgICAgICAgICAgICAgInIiIDogIjMiLA0KICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiIiwidmFsdWVzZXQgIiwiXCJPZmZpY2UgVmlzaXRcIiIsIjogIiwiJ2h0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjQ2NC4xMDAzLjEwMS4xMi4xMDAxJyIgXQ0KICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICI0IiwNCiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIxODoxLTE4OjEwMyIsDQogICAgICAgICAgICAibmFtZSIgOiAiQW5udWFsIFdlbGxuZXNzIFZpc2l0IiwNCiAgICAgICAgICAgICJpZCIgOiAiaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNTI2LjMuMTI0MCIsDQogICAgICAgICAgICAiYWNjZXNzTGV2ZWwiIDogIlB1YmxpYyIsDQogICAgICAgICAgICAiYW5ub3RhdGlvbiIgOiBbIHsNCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJBbm5vdGF0aW9uIiwNCiAgICAgICAgICAgICAgICJzIiA6IHsNCiAgICAgICAgICAgICAgICAgICJyIiA6ICI0IiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsInZhbHVlc2V0ICIsIlwiQW5udWFsIFdlbGxuZXNzIFZpc2l0XCIiLCI6ICIsIidodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy41MjYuMy4xMjQwJyIgXQ0KICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICI1IiwNCiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIxOToxLTE5OjE1NCIsDQogICAgICAgICAgICAibmFtZSIgOiAiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzIC0gRXN0YWJsaXNoZWQgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiLA0KICAgICAgICAgICAgImlkIiA6ICJodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyNSIsDQogICAgICAgICAgICAiYWNjZXNzTGV2ZWwiIDogIlB1YmxpYyIsDQogICAgICAgICAgICAiYW5ub3RhdGlvbiIgOiBbIHsNCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJBbm5vdGF0aW9uIiwNCiAgICAgICAgICAgICAgICJzIiA6IHsNCiAgICAgICAgICAgICAgICAgICJyIiA6ICI1IiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsInZhbHVlc2V0ICIsIlwiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzIC0gRXN0YWJsaXNoZWQgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXBcIiIsIjogIiwiJ2h0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjQ2NC4xMDAzLjEwMS4xMi4xMDI1JyIgXQ0KICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICI2IiwNCiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyMDoxLTIwOjE0OCIsDQogICAgICAgICAgICAibmFtZSIgOiAiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzLUluaXRpYWwgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiLA0KICAgICAgICAgICAgImlkIiA6ICJodHRwOi8vY3RzLm5sbS5uaWguZ292L2ZoaXIvVmFsdWVTZXQvMi4xNi44NDAuMS4xMTM4ODMuMy40NjQuMTAwMy4xMDEuMTIuMTAyMyIsDQogICAgICAgICAgICAiYWNjZXNzTGV2ZWwiIDogIlB1YmxpYyIsDQogICAgICAgICAgICAiYW5ub3RhdGlvbiIgOiBbIHsNCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJBbm5vdGF0aW9uIiwNCiAgICAgICAgICAgICAgICJzIiA6IHsNCiAgICAgICAgICAgICAgICAgICJyIiA6ICI2IiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsInZhbHVlc2V0ICIsIlwiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzLUluaXRpYWwgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXBcIiIsIjogIiwiJ2h0dHA6Ly9jdHMubmxtLm5paC5nb3YvZmhpci9WYWx1ZVNldC8yLjE2Ljg0MC4xLjExMzg4My4zLjQ2NC4xMDAzLjEwMS4xMi4xMDIzJyIgXQ0KICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9IF0NCiAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICI3IiwNCiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyMToxLTIxOjExNiIsDQogICAgICAgICAgICAibmFtZSIgOiAiSG9tZSBIZWFsdGhjYXJlIFNlcnZpY2VzIiwNCiAgICAgICAgICAgICJpZCIgOiAiaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMTYiLA0KICAgICAgICAgICAgImFjY2Vzc0xldmVsIiA6ICJQdWJsaWMiLA0KICAgICAgICAgICAgImFubm90YXRpb24iIDogWyB7DQogICAgICAgICAgICAgICAidHlwZSIgOiAiQW5ub3RhdGlvbiIsDQogICAgICAgICAgICAgICAicyIgOiB7DQogICAgICAgICAgICAgICAgICAiciIgOiAiNyIsDQogICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICIiLCJ2YWx1ZXNldCAiLCJcIkhvbWUgSGVhbHRoY2FyZSBTZXJ2aWNlc1wiIiwiOiAiLCInaHR0cDovL2N0cy5ubG0ubmloLmdvdi9maGlyL1ZhbHVlU2V0LzIuMTYuODQwLjEuMTEzODgzLjMuNDY0LjEwMDMuMTAxLjEyLjEwMTYnIiBdDQogICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0gXQ0KICAgICAgICAgfSBdDQogICAgICB9LA0KICAgICAgImNvbnRleHRzIiA6IHsNCiAgICAgICAgICJkZWYiIDogWyB7DQogICAgICAgICAgICAibG9jYXRvciIgOiAiMjY6MS0yNjoxNSIsDQogICAgICAgICAgICAibmFtZSIgOiAiUGF0aWVudCINCiAgICAgICAgIH0gXQ0KICAgICAgfSwNCiAgICAgICJzdGF0ZW1lbnRzIiA6IHsNCiAgICAgICAgICJkZWYiIDogWyB7DQogICAgICAgICAgICAibG9jYXRvciIgOiAiMjY6MS0yNjoxNSIsDQogICAgICAgICAgICAibmFtZSIgOiAiUGF0aWVudCIsDQogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsDQogICAgICAgICAgICAiZXhwcmVzc2lvbiIgOiB7DQogICAgICAgICAgICAgICAidHlwZSIgOiAiU2luZ2xldG9uRnJvbSIsDQogICAgICAgICAgICAgICAib3BlcmFuZCIgOiB7DQogICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjY6MS0yNjoxNSIsDQogICAgICAgICAgICAgICAgICAiZGF0YVR5cGUiIDogIntodHRwOi8vaGw3Lm9yZy9maGlyfVBhdGllbnQiLA0KICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9QYXRpZW50IiwNCiAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJSZXRyaWV2ZSINCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICJsb2NhbElkIiA6ICIzNCIsDQogICAgICAgICAgICAibG9jYXRvciIgOiAiMjg6MS0zNzo0MyIsDQogICAgICAgICAgICAibmFtZSIgOiAiUXVhbGlmeWluZyBFbmNvdW50ZXJzIiwNCiAgICAgICAgICAgICJjb250ZXh0IiA6ICJQYXRpZW50IiwNCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwNCiAgICAgICAgICAgICJhbm5vdGF0aW9uIiA6IFsgew0KICAgICAgICAgICAgICAgInR5cGUiIDogIkFubm90YXRpb24iLA0KICAgICAgICAgICAgICAgInMiIDogew0KICAgICAgICAgICAgICAgICAgInIiIDogIjM0IiwNCiAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIiIsImRlZmluZSAiLCJcIlF1YWxpZnlpbmcgRW5jb3VudGVyc1wiIiwiOlxuXHQiIF0NCiAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIzMyIsDQogICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjIzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjIyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIihcbiAgICAiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIyMiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIyMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIxOCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIxNiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIxNCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJbIiwiRW5jb3VudGVyIiwiOiAiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlwiT2ZmaWNlIFZpc2l0XCIiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIl0iIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlxuICBcdFx0dW5pb24gIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMTUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiWyIsIkVuY291bnRlciIsIjogIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJcIkFubnVhbCBXZWxsbmVzcyBWaXNpdFwiIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJdIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlxuICBcdFx0dW5pb24gIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMTciLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiWyIsIkVuY291bnRlciIsIjogIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJcIlByZXZlbnRpdmUgQ2FyZSBTZXJ2aWNlcyAtIEVzdGFibGlzaGVkIE9mZmljZSBWaXNpdCwgMTggYW5kIFVwXCIiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIl0iIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiXG4gIFx0XHR1bmlvbiAiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIxOSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJbIiwiRW5jb3VudGVyIiwiOiAiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlwiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzLUluaXRpYWwgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXBcIiIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiXSIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJcbiAgXHRcdHVuaW9uICIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjIxIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlsiLCJFbmNvdW50ZXIiLCI6ICIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiXCJIb21lIEhlYWx0aGNhcmUgU2VydmljZXNcIiIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiXSIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJcbiAgKSIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiICIsIlZhbGlkRW5jb3VudGVyIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlxuXHRcdCIgXQ0KICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjMyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIndoZXJlICIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjMyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjI3IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjI1IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjI0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlZhbGlkRW5jb3VudGVyIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICIuIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMjUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAicGVyaW9kIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjI3IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICIgIiwiZHVyaW5nIiwiICIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjI2IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIlwiTWVhc3VyZW1lbnQgUGVyaW9kXCIiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiXG4gIFx0XHRhbmQgIiBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMzEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMjkiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiciIgOiAiMjgiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInMiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiVmFsaWRFbmNvdW50ZXIiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIi4iIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyIiA6ICIyOSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicyIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiBbICJzdGF0dXMiIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidmFsdWUiIDogWyAiICAiLCI9IiwiICIgXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInIiIDogIjMwIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInZhbHVlIiA6IFsgIidmaW5pc2hlZCciIF0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0gXSwNCiAgICAgICAgICAgICJleHByZXNzaW9uIiA6IHsNCiAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIzMyIsDQogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjk6Mi0zNzo0MyIsDQogICAgICAgICAgICAgICAidHlwZSIgOiAiUXVlcnkiLA0KICAgICAgICAgICAgICAgInNvdXJjZSIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIyMyIsDQogICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjk6Mi0zNToxOCIsDQogICAgICAgICAgICAgICAgICAiYWxpYXMiIDogIlZhbGlkRW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICJleHByZXNzaW9uIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIyMiIsDQogICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjk6Mi0zNTozIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJVbmlvbiIsDQogICAgICAgICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIyMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzA6NS0zMzo4MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiVW5pb24iLA0KICAgICAgICAgICAgICAgICAgICAgICAgIm9wZXJhbmQiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMTYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjMwOjUtMzE6NDYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlVuaW9uIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJvcGVyYW5kIiA6IFsgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImxvY2FsSWQiIDogIjE0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMDo1LTMwOjMxIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9RW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0ZW1wbGF0ZUlkIiA6ICJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vRW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb2RlUHJvcGVydHkiIDogInR5cGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVDb21wYXJhdG9yIiA6ICJpbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVzIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMDoxNy0zMDozMCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSIgOiAiT2ZmaWNlIFZpc2l0IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJWYWx1ZVNldFJlZiINCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIxNSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzE6MTEtMzE6NDYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImRhdGFUeXBlIiA6ICJ7aHR0cDovL2hsNy5vcmcvZmhpcn1FbmNvdW50ZXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9FbmNvdW50ZXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVQcm9wZXJ0eSIgOiAidHlwZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiY29kZUNvbXBhcmF0b3IiIDogImluIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJSZXRyaWV2ZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiY29kZXMiIDogew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjMxOjIzLTMxOjQ1IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJBbm51YWwgV2VsbG5lc3MgVmlzaXQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlZhbHVlU2V0UmVmIg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiVW5pb24iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIm9wZXJhbmQiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMTciLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjMyOjExLTMyOjg3IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9RW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0ZW1wbGF0ZUlkIiA6ICJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vRW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb2RlUHJvcGVydHkiIDogInR5cGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVDb21wYXJhdG9yIiA6ICJpbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVzIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMjoyMy0zMjo4NiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSIgOiAiUHJldmVudGl2ZSBDYXJlIFNlcnZpY2VzIC0gRXN0YWJsaXNoZWQgT2ZmaWNlIFZpc2l0LCAxOCBhbmQgVXAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlZhbHVlU2V0UmVmIg0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImxvY2FsSWQiIDogIjE5IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMzoxMS0zMzo4MSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGF0YVR5cGUiIDogIntodHRwOi8vaGw3Lm9yZy9maGlyfUVuY291bnRlciIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAidGVtcGxhdGVJZCIgOiAiaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0VuY291bnRlciIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiY29kZVByb3BlcnR5IiA6ICJ0eXBlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb2RlQ29tcGFyYXRvciIgOiAiaW4iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlJldHJpZXZlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjb2RlcyIgOiB7DQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzM6MjMtMzM6ODAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIm5hbWUiIDogIlByZXZlbnRpdmUgQ2FyZSBTZXJ2aWNlcy1Jbml0aWFsIE9mZmljZSBWaXNpdCwgMTggYW5kIFVwIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJWYWx1ZVNldFJlZiINCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMjEiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjM0OjExLTM0OjQ5IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9RW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ0ZW1wbGF0ZUlkIiA6ICJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vRW5jb3VudGVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJjb2RlUHJvcGVydHkiIDogInR5cGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVDb21wYXJhdG9yIiA6ICJpbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImNvZGVzIiA6IHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzNDoyMy0zNDo0OCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAibmFtZSIgOiAiSG9tZSBIZWFsdGhjYXJlIFNlcnZpY2VzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJWYWx1ZVNldFJlZiINCiAgICAgICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgfSBdLA0KICAgICAgICAgICAgICAgInJlbGF0aW9uc2hpcCIgOiBbIF0sDQogICAgICAgICAgICAgICAid2hlcmUiIDogew0KICAgICAgICAgICAgICAgICAgImxvY2FsSWQiIDogIjMyIiwNCiAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzNjozLTM3OjQzIiwNCiAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJBbmQiLA0KICAgICAgICAgICAgICAgICAgIm9wZXJhbmQiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMjciLA0KICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjM2OjktMzY6NTciLA0KICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkluY2x1ZGVkSW4iLA0KICAgICAgICAgICAgICAgICAgICAgIm9wZXJhbmQiIDogWyB7DQogICAgICAgICAgICAgICAgICAgICAgICAibmFtZSIgOiAiVG9JbnRlcnZhbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAibGlicmFyeU5hbWUiIDogIkZISVJIZWxwZXJzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJGdW5jdGlvblJlZiIsDQogICAgICAgICAgICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIyNSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzY6OS0zNjoyOSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAicGF0aCIgOiAicGVyaW9kIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY29wZSIgOiAiVmFsaWRFbmNvdW50ZXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlByb3BlcnR5Ig0KICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMjYiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjM2OjM4LTM2OjU3IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJNZWFzdXJlbWVudCBQZXJpb2QiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlBhcmFtZXRlclJlZiINCiAgICAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgICAgfSwgew0KICAgICAgICAgICAgICAgICAgICAgImxvY2FsSWQiIDogIjMxIiwNCiAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzNzo5LTM3OjQzIiwNCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJFcXVhbCIsDQogICAgICAgICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJUb1N0cmluZyIsDQogICAgICAgICAgICAgICAgICAgICAgICAibGlicmFyeU5hbWUiIDogIkZISVJIZWxwZXJzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJGdW5jdGlvblJlZiIsDQogICAgICAgICAgICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJsb2NhbElkIiA6ICIyOSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzc6OS0zNzoyOSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAicGF0aCIgOiAic3RhdHVzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJzY29wZSIgOiAiVmFsaWRFbmNvdW50ZXIiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlByb3BlcnR5Ig0KICAgICAgICAgICAgICAgICAgICAgICAgfSBdDQogICAgICAgICAgICAgICAgICAgICB9LCB7DQogICAgICAgICAgICAgICAgICAgICAgICAibG9jYWxJZCIgOiAiMzAiLA0KICAgICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjM3OjM0LTM3OjQzIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZVR5cGUiIDogInt1cm46aGw3LW9yZzplbG0tdHlwZXM6cjF9U3RyaW5nIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICJ2YWx1ZSIgOiAiZmluaXNoZWQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiIDogIkxpdGVyYWwiDQogICAgICAgICAgICAgICAgICAgICB9IF0NCiAgICAgICAgICAgICAgICAgIH0gXQ0KICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgfQ0KICAgICAgICAgfSBdDQogICAgICB9DQogICB9DQp9\"\n" + + " } ]\n" + + "}"; +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundleIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundleIT.java new file mode 100644 index 000000000..4b2d98806 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/mat/ExtractMatBundleIT.java @@ -0,0 +1,200 @@ +package org.opencds.cqf.tooling.operations.mat; + +import ca.uhn.fhir.context.FhirContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Collections; + +public class ExtractMatBundleIT { + + @Test + void testDSTU3ExtractMatBundleOperation() { + ExtractMatBundle extractMatBundle = new ExtractMatBundle(); + extractMatBundle.setFhirContext(FhirContext.forDstu3Cached()); + + org.hl7.fhir.dstu3.model.Bundle dstu3MatBundle = new org.hl7.fhir.dstu3.model.Bundle(); + + org.hl7.fhir.dstu3.model.Library dstu3MatLibrary = new org.hl7.fhir.dstu3.model.Library(); + dstu3MatLibrary.setId("dstu3-test-mat-library"); + dstu3MatLibrary.setName("DSTU3_TEST_MAT_LIBRARY"); + dstu3MatLibrary.addContent().setContentType("text/cql").setData(DSTU3TestMatLibrary.getBytes()); + + org.hl7.fhir.dstu3.model.Measure dstu3MatMeasure = new org.hl7.fhir.dstu3.model.Measure(); + dstu3MatMeasure.setId("dstu3-test-mat-measure"); + dstu3MatMeasure.setName("DSTU3_TEST_MAT_MEASURE"); + dstu3MatMeasure.setLibrary(Collections.singletonList(new org.hl7.fhir.dstu3.model.Reference("Library/dstu3-test-mat-library"))); + + dstu3MatBundle.addEntry().setResource(dstu3MatLibrary); + dstu3MatBundle.addEntry().setResource(dstu3MatMeasure); + + ExtractMatBundle.MatPackage matPackage = extractMatBundle.getMatPackage(dstu3MatBundle); + Assert.assertFalse(matPackage.getLibraryPackages().isEmpty()); + Assert.assertNotNull(matPackage.getLibraryPackages().get(0).getCql()); + Assert.assertFalse(matPackage.getMeasures().isEmpty()); + Assert.assertTrue(matPackage.getOtherResources().isEmpty()); + } + + @Test + void testR4ExtractMatBundleOperation() { + ExtractMatBundle extractMatBundle = new ExtractMatBundle(); + extractMatBundle.setFhirContext(FhirContext.forR4Cached()); + + org.hl7.fhir.r4.model.Bundle r4MatBundle = new org.hl7.fhir.r4.model.Bundle(); + + org.hl7.fhir.r4.model.Library r4MatLibrary = new org.hl7.fhir.r4.model.Library(); + r4MatLibrary.setId("r4-test-mat-library"); + r4MatLibrary.setName("R4_TEST_MAT_LIBRARY"); + r4MatLibrary.addContent().setContentType("text/cql").setData(R4TestMatLibrary.getBytes()); + + org.hl7.fhir.r4.model.Measure r4MatMeasure = new org.hl7.fhir.r4.model.Measure(); + r4MatMeasure.setId("r4-test-mat-measure"); + r4MatMeasure.setName("R4_TEST_MAT_MEASURE"); + r4MatMeasure.setLibrary(Collections.singletonList(new org.hl7.fhir.r4.model.CanonicalType("Library/r4-test-mat-library"))); + + r4MatBundle.addEntry().setResource(r4MatLibrary); + r4MatBundle.addEntry().setResource(r4MatMeasure); + + ExtractMatBundle.MatPackage matPackage = extractMatBundle.getMatPackage(r4MatBundle); + Assert.assertFalse(matPackage.getLibraryPackages().isEmpty()); + Assert.assertNotNull(matPackage.getLibraryPackages().get(0).getCql()); + Assert.assertFalse(matPackage.getMeasures().isEmpty()); + Assert.assertTrue(matPackage.getOtherResources().isEmpty()); + } + + private final String DSTU3TestMatLibrary = "library FHIRHelpers version '1.8'\n" + + "\n" + + "using FHIR version '1.8'\n" + + "\n" + + "define function ToInterval(period FHIR.Period):\n" + + " Interval[period.\"start\".value, period.\"end\".value]\n" + + "\n" + + "define function ToQuantity(quantity FHIR.Quantity):\n" + + " System.Quantity { value: quantity.value.value, unit: quantity.unit.value }\n" + + "\n" + + "define function ToInterval(range FHIR.Range):\n" + + " Interval[ToQuantity(range.low), ToQuantity(range.high)]\n" + + "\n" + + "define function ToCode(coding FHIR.Coding):\n" + + " System.Code {\n" + + " code: coding.code.value,\n" + + " system: coding.system.value,\n" + + " version: coding.version.value,\n" + + " display: coding.display.value\n" + + " }\n" + + "\n" + + "define function ToConcept(concept FHIR.CodeableConcept):\n" + + " System.Concept {\n" + + " codes: concept.coding C return ToCode(C),\n" + + " display: concept.text.value\n" + + " }"; + + private final String R4TestMatLibrary = "library R4_TEST_MAT_LIBRARY version '4.0.1'\n" + + "\n" + + "using FHIR version '4.0.1'\n" + + "\n" + + "define function ToInterval(period FHIR.Period):\n" + + " if period is null then\n" + + " null\n" + + " else\n" + + " if period.\"start\" is null then\n" + + " Interval(period.\"start\".value, period.\"end\".value]\n" + + " else\n" + + " Interval[period.\"start\".value, period.\"end\".value]\n" + + "\n" + + "define function ToCalendarUnit(unit System.String):\n" + + " case unit\n" + + " when 'ms' then 'millisecond'\n" + + " when 's' then 'second'\n" + + " when 'min' then 'minute'\n" + + " when 'h' then 'hour'\n" + + " when 'd' then 'day'\n" + + " when 'wk' then 'week'\n" + + " when 'mo' then 'month'\n" + + " when 'a' then 'year'\n" + + " else unit\n" + + " end\n" + + "\n" + + "define function ToQuantity(quantity FHIR.Quantity):\n" + + " case\n" + + " when quantity is null then null\n" + + " when quantity.value is null then null\n" + + " when quantity.comparator is not null then\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.ComparatorQuantityNotSupported', 'Error', 'FHIR Quantity value has a comparator and cannot be converted to a System.Quantity value.')\n" + + " when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org'\n" + + " or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then\n" + + " System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) }\n" + + " else\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')')\n" + + " end\n" + + "\n" + + "define function ToQuantityIgnoringComparator(quantity FHIR.Quantity):\n" + + " case\n" + + " when quantity is null then null\n" + + " when quantity.value is null then null\n" + + " when quantity.system is null or quantity.system.value = 'http://unitsofmeasure.org'\n" + + " or quantity.system.value = 'http://hl7.org/fhirpath/CodeSystem/calendar-units' then\n" + + " System.Quantity { value: quantity.value.value, unit: ToCalendarUnit(Coalesce(quantity.code.value, quantity.unit.value, '1')) }\n" + + " else\n" + + " Message(null, true, 'FHIRHelpers.ToQuantity.InvalidFHIRQuantity', 'Error', 'Invalid FHIR Quantity code: ' & quantity.unit.value & ' (' & quantity.system.value & '|' & quantity.code.value & ')')\n" + + " end\n" + + "\n" + + "define function ToInterval(quantity FHIR.Quantity):\n" + + " if quantity is null then null else\n" + + " case quantity.comparator.value\n" + + " when '<' then\n" + + " Interval[\n" + + " null,\n" + + " ToQuantityIgnoringComparator(quantity)\n" + + " )\n" + + " when '<=' then\n" + + " Interval[\n" + + " null,\n" + + " ToQuantityIgnoringComparator(quantity)\n" + + " ]\n" + + " when '>=' then\n" + + " Interval[\n" + + " ToQuantityIgnoringComparator(quantity),\n" + + " null\n" + + " ]\n" + + " when '>' then\n" + + " Interval(\n" + + " ToQuantityIgnoringComparator(quantity),\n" + + " null\n" + + " ]\n" + + " else\n" + + " Interval[ToQuantity(quantity), ToQuantity(quantity)]\n" + + " end\n" + + "\n" + + "define function ToRatio(ratio FHIR.Ratio):\n" + + " if ratio is null then\n" + + " null\n" + + " else\n" + + " System.Ratio { numerator: ToQuantity(ratio.numerator), denominator: ToQuantity(ratio.denominator) }\n" + + "\n" + + "define function ToInterval(range FHIR.Range):\n" + + " if range is null then\n" + + " null\n" + + " else\n" + + " Interval[ToQuantity(range.low), ToQuantity(range.high)]\n" + + "\n" + + "define function ToCode(coding FHIR.Coding):\n" + + " if coding is null then\n" + + " null\n" + + " else\n" + + " System.Code {\n" + + " code: coding.code.value,\n" + + " system: coding.system.value,\n" + + " version: coding.version.value,\n" + + " display: coding.display.value\n" + + " }\n" + + "\n" + + "define function ToConcept(concept FHIR.CodeableConcept):\n" + + " if concept is null then\n" + + " null\n" + + " else\n" + + " System.Concept {\n" + + " codes: concept.coding C return ToCode(C),\n" + + " display: concept.text.value\n" + + " }"; +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformanceIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformanceIT.java new file mode 100644 index 000000000..579645a3b --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/validation/DataProfileConformanceIT.java @@ -0,0 +1,78 @@ +package org.opencds.cqf.tooling.operations.validation; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Patient; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +public class DataProfileConformanceIT { + + private final FhirContext fhirContext = FhirContext.forR4Cached(); + + @Test + void testInvalidQiCorePatient() { + DataProfileConformance dpc = new DataProfileConformance(); + dpc.setFhirContext(fhirContext); + dpc.setPackageUrlsList(List.of("http://hl7.org/fhir/us/qicore/4.1.1/package.tgz")); + dpc.setGeneralValidator(); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.COLLECTION); + bundle.addEntry().setResource(invalidQiCorePatient()); + + List<IBaseResource> validationResults = dpc.validatePatientData(bundle); + Assert.assertEquals(validationResults.size(), 1); + Assert.assertTrue(validationResults.get(0) instanceof Patient); + Patient patient = (Patient) validationResults.get(0); + Assert.assertTrue(patient.hasContained()); + Assert.assertEquals(patient.getContained().size(), 1); + Assert.assertTrue(patient.getContained().get(0) instanceof OperationOutcome); + OperationOutcome outcome = (OperationOutcome) patient.getContained().get(0); + Assert.assertTrue(outcome.hasIssue()); + Assert.assertEquals(outcome.getIssue().size(), 2); + } + + @Test + void testValidQiCorePatient() { + DataProfileConformance dpc = new DataProfileConformance(); + dpc.setFhirContext(fhirContext); + dpc.setPackageUrlsList(List.of("http://hl7.org/fhir/us/qicore/4.1.1/package.tgz")); + dpc.setGeneralValidator(); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.COLLECTION); + bundle.addEntry().setResource(validQiCorePatient()); + + List<IBaseResource> validationResults = dpc.validatePatientData(bundle); + Assert.assertEquals(validationResults.size(), 1); + Assert.assertTrue(validationResults.get(0) instanceof Patient); + Patient patient = (Patient) validationResults.get(0); + Assert.assertFalse(patient.hasContained()); + } + + private Patient invalidQiCorePatient() { + // missing identifier and name + Patient patient = new Patient(); + patient.setId("invalid"); + patient.setGender(Enumerations.AdministrativeGender.FEMALE); + + return patient; + } + + private Patient validQiCorePatient() { + // missing identifier and name + Patient patient = new Patient(); + patient.setId("valid"); + patient.addIdentifier().setSystem("urn:oid:1.2.36.146.595.217.0.1").setValue("12345"); + patient.addName().setFamily("Chalmers").addGiven("Peter").addGiven("James"); + patient.setGender(Enumerations.AdministrativeGender.FEMALE); + + return patient; + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operations/valuest/generate/config/ConfigValueSetGeneratorIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/operations/valuest/generate/config/ConfigValueSetGeneratorIT.java new file mode 100644 index 000000000..bbb8b65b5 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operations/valuest/generate/config/ConfigValueSetGeneratorIT.java @@ -0,0 +1,68 @@ +package org.opencds.cqf.tooling.operations.valuest.generate.config; + +import ca.uhn.fhir.context.FhirContext; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.operations.valueset.generate.config.Config; +import org.opencds.cqf.tooling.operations.valueset.generate.config.ConfigValueSetGenerator; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +public class ConfigValueSetGeneratorIT { + + private final FhirContext fhirContext = FhirContext.forR4Cached(); + + @Test + void testSimpleConfigRulesText() throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + Config config = mapper.readValue(SIMPLE_CONFIG, Config.class); + ConfigValueSetGenerator configValueSetGenerator = new ConfigValueSetGenerator(); + configValueSetGenerator.setFhirContext(fhirContext); + List<IBaseResource> valueSets = configValueSetGenerator.generateValueSets(config); + Assert.assertFalse(valueSets.isEmpty()); + } + + private final String SIMPLE_CONFIG = "{\n" + + " \"author\": {\n" + + " \"name\": \"MD Partners, Inc.\",\n" + + " \"contactType\": \"email\",\n" + + " \"contactValue\": \"info@mdpartners.com\"\n" + + " },\n" + + " \"valuesets\": [\n" + + " {\n" + + " \"id\": \"opioid-analgesics-with-ambulatory-misuse-potential\",\n" + + " \"canonical\": \"http://fhir.org/guides/cdc/opioid-cds/ValueSet/opioid-analgesics-with-ambulatory-misuse-potential\",\n" + + " \"title\": \"Opioid analgesics with ambulatory misuse potential\",\n" + + " \"description\": \"All opioid clinical drugs except cough medications, antispasmodics, or those restricted to surgical use only as identified by those using an injectable form.\",\n" + + " \"purpose\": \"Opioid medications that should have opioid management CDS\",\n" + + " \"clinicalFocus\": \"All opioid clinical drugs except cough medications, antispasmodics, or those restricted to surgical use only.\",\n" + + " \"dataElementScope\": \"Medication\",\n" + + " \"inclusionCriteria\": \"All opioid-class medications\",\n" + + " \"exclusionCriteria\": \"All medications including ingredients intended to treat cough or act as an antispasmodic. All injectable forms.\",\n" + + " \"rulesText\": {\n" + + " \"narrative\": \"https:\\/\\/mor.nlm.nih.gov\\/RxMix\\/ Script:\\r\\nStep 1a \\r\\nCreate Batch text input file (SCT-Opioids.txt) with following SCT identifier (for the concept \\\"Product containing opioid receptor agonist (product)\\\") as an input within the file: \\r\\n360204007 \\r\\n\\r\\nStep 1b\\r\\nSubmit batch job using the above SCT-Opioids.txt file to following workflow by uploading file (SCT-Opioid-wf.config) with the following in the file: <WFE><filteredOutputs>RXCUI|name|term_type<\\/filteredOutputs><input>NOINPUT<\\/input><FS><service>NOINPUT<\\/service><function>findClassById<\\/function><level>0<\\/level><paramSize>1<\\/paramSize><param order ='0'>?<\\/param><\\/FS><FS><service>NOINPUT<\\/service><function>getClassMembers<\\/function><level>1<\\/level><paramSize>5<\\/paramSize><param order ='0'>?<\\/param><param order ='1'>SNOMEDCT<\\/param><param order ='2'>isa_disposition<\\/param><param order ='3'>0<\\/param><param order ='4'>IN,MIN,PIN<\\/param><\\/FS><FS><service>NOINPUT<\\/service><function>getRelatedByType<\\/function><level>2<\\/level><paramSize>2<\\/paramSize><param order ='0'>?<\\/param><param order ='1'>BPCK,GPCK,SBD,SCD<\\/param><\\/FS><\\/WFE>\\r\\nThis will produce a result file with all Opioid clinical drugs included\\r\\n\\r\\nStep 2\\r\\nTo remove all cough and bowel transit formulation codes and to remove the injectable codes filter out all codes with the following strings:\\r\\nIngredient strings: \\r\\nGuaifenesin, Chlorpheniramine, Pseudoephedrine, Brompheniramine, Phenylephrine, Phenylpropanolamine, Promethazine, Bromodiphenhydramine, guaiacolsulfonate, homatropine\\r\\nForm strings:\\r\\ninject, cartridge, syringe\",\n" + + " \"workflowXml\": \"<WFE><filteredOutputs>RXCUI|name|term_type</filteredOutputs><input>NOINPUT</input><FS><service>NOINPUT</service><function>findClassById</function><level>0</level><paramSize>1</paramSize><param order ='0'>?</param></FS><FS><service>NOINPUT</service><function>getClassMembers</function><level>1</level><paramSize>5</paramSize><param order ='0'>?</param><param order ='1'>SNOMEDCT</param><param order ='2'>isa_disposition</param><param order ='3'>0</param><param order ='4'>IN,MIN,PIN</param></FS><FS><service>NOINPUT</service><function>getRelatedByType</function><level>2</level><paramSize>2</paramSize><param order ='0'>?</param><param order ='1'>BPCK,GPCK,SBD,SCD</param></FS></WFE>\",\n" + + " \"input\": [ \"360204007\" ],\n" + + " \"excludeFilter\": [\n" + + " \"Guaifenesin\",\n" + + " \"Chlorpheniramine\",\n" + + " \"Pseudoephedrine\",\n" + + " \"Brompheniramine\",\n" + + " \"Phenylephrine\",\n" + + " \"Phenylpropanolamine\",\n" + + " \"Promethazine\",\n" + + " \"Bromodiphenhydramine\",\n" + + " \"guaiacolsulfonate\",\n" + + " \"homatropine\",\n" + + " \"inject\",\n" + + " \"cartridge\",\n" + + " \"syringe\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/utilities/IDUtilsTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/IDUtilsTest.java new file mode 100644 index 000000000..46a620ded --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/IDUtilsTest.java @@ -0,0 +1,43 @@ +package org.opencds.cqf.tooling.utilities; + +import org.junit.Assert; +import org.junit.Test; +import org.opencds.cqf.tooling.exception.InvalidIdException; + +public class IDUtilsTest { + + @Test + public void testValidateId_Valid() { + String[] alphanumericIds = { + "4b6c5c3f-9252-413c-a231-a5d0d8f2edcc", + "a1", + "abcdef-ghijkl-mnopqrs-tuvwxyz", + "valid-id-1", + }; + for (String validId: alphanumericIds) { + IDUtils.validateId(validId, false); + } + + String[] numericIds = { + "12345-67891-01112-12131", + "0", + }; + for (String validId: numericIds) { + IDUtils.validateId(validId, true); + } + } + + @Test + public void testValidateId_Invalid() { + String[] invalidIds = { + "12345-67891-01112-12131", + "0", + "", + "4b6c5c3f-9252-413c-a231-a5d0d8f2edcc-4b6c5c3f-9252-413c-a231-a5d0d8f2edcc-4b6c5c3f-9252-413c-a231", + + }; + for (String invalidId: invalidIds) { + Assert.assertThrows(InvalidIdException.class, () -> IDUtils.validateId(invalidId, false)); + } + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/utilities/OperationUtilsIT.java b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/OperationUtilsIT.java new file mode 100644 index 000000000..93165ff33 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/OperationUtilsIT.java @@ -0,0 +1,53 @@ +package org.opencds.cqf.tooling.utilities; + +import org.opencds.cqf.tooling.operations.bundle.BundleResources; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class OperationUtilsIT { + + @Test + void testGetParamTypeFromMethod() { + BundleResources bundleResources = new BundleResources(); + Class<?> clazz = OperationUtils.getParamType(bundleResources, "setOutputPath"); + Assert.assertTrue(clazz.isAssignableFrom(String.class)); + } + + @Test + void testMapParamType() { + String testString = "testString"; + Integer testInteger = 1; + Boolean testBoolean = true; + String stringMapResult = OperationUtils.mapParamType(testString, String.class); + Integer integerMapResult = OperationUtils.mapParamType("1", Integer.class); + Boolean booleanMapResult = OperationUtils.mapParamType("true", Boolean.class); + + Assert.assertEquals(stringMapResult, testString); + Assert.assertEquals(integerMapResult, testInteger); + Assert.assertEquals(booleanMapResult, testBoolean); + } + + @Test + void testHelpMenu() { + String helpMenu = OperationUtils.getHelpMenu(new BundleResources()); + Assert.assertNotNull(helpMenu); + Assert.assertEquals(helpMenu, bundleResourcesHelpMenu); + } + + private final String bundleResourcesHelpMenu = + "╔═════════════════════════╤════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n" + + "║ Parameter │ Description ║\n" + + "╠═════════════════════════╪════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n" + + "║ -ptr | -pathtoresources │ Path to the directory containing the resource files to be consolidated into the new bundle (required) ║\n" + + "╟─────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢\n" + + "║ -e | -encoding │ The file format to be used for representing the resulting Bundle { json, xml } (default json) ║\n" + + "╟─────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢\n" + + "║ -v | -version │ FHIR version { stu3, r4, r5 } (default r4) ║\n" + + "╟─────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢\n" + + "║ -t | -type │ The Bundle type as defined in the FHIR specification for the Bundle.type element (default transaction) ║\n" + + "╟─────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢\n" + + "║ -bid | -bundleid │ A valid FHIR ID to be used as the ID for the resulting FHIR Bundle (optional) ║\n" + + "╟─────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢\n" + + "║ -op | -outputPath │ The directory path to which the generated Bundle file should be written (default src/main/resources/org/opencds/cqf/tooling/bundle/output) ║\n" + + "╚═════════════════════════╧════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n"; +}