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 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 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 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 currentActionSubs = currentAction.getAction();
+ List 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 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 left,
+ List 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 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 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 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 bundleTransaction(@Nonnull List bundles,
+ @Nonnull FhirContext fhirContext,
+ @Nonnull String fhirServerUri) {
+ IGenericClient client = fhirContext.newRestfulGenericClient(fhirServerUri);
+ AtomicReference response = new AtomicReference<>();
+ List 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 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 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 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 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 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 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 getInputs() {
+ if (inputs == null) {
+ inputs = Arrays.stream(input.split(",")).map(String::trim).collect(Collectors.toList());
+ }
+ return inputs;
+ }
+
+ public void setInputs(List inputs) {
+ this.inputs = inputs;
+ }
+
+ public void setInput(String input) {
+ this.input = input;
+ }
+
+ public List getIncludeFilters() {
+ if (includeFilters == null && includeFilter != null) {
+ includeFilters = Arrays.stream(includeFilter.split(",")).map(String::trim).collect(Collectors.toList());
+ }
+ return includeFilters;
+ }
+
+ public void setIncludeFilters(List includeFilters) {
+ this.includeFilters = includeFilters;
+ }
+
+ public void setIncludeFilter(String includeFilter) {
+ this.includeFilter = includeFilter;
+ }
+
+ public List getExcludeFilters() {
+ if (excludeFilters == null && excludeFilter != null) {
+ excludeFilters = Arrays.stream(excludeFilter.split(",")).map(String::trim).collect(Collectors.toList());
+ }
+ return excludeFilters;
+ }
+
+ public void setExcludeFilters(List 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 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 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> 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> getDateClasses(FhirContext fhirContext) {
+ List> 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 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 resources = BundleUtil.toListOfResources(fhirContext, bundle);
+ createDirectoryStructure();
+ migrateResources(resources);
+ }
+
+ // Library access method
+ public MatPackage getMatPackage(IBaseBundle bundle) {
+ MatPackage matPackage = new MatPackage();
+ List 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 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 libraryPackages;
+ private final List measures;
+ private final List otherResources;
+
+ public MatPackage() {
+ this.libraryPackages = new ArrayList<>();
+ this.measures = new ArrayList<>();
+ this.otherResources = new ArrayList<>();
+ }
+
+ public List getLibraryPackages() {
+ return libraryPackages;
+ }
+
+ public void addLibraryPackage(LibraryPackage libraryPackage) {
+ this.libraryPackages.add(libraryPackage);
+ }
+
+ public List getMeasures() {
+ return measures;
+ }
+
+ public void addMeasure(IBaseResource measure) {
+ this.measures.add(measure);
+ }
+
+ public List 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 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 validatePatientData(IBaseBundle patientData) {
+ List 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> profileMap = new HashMap<>();
+ private void populateProfileMap(List 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 getPackageUrlsList() {
+ if (packageUrlsList == null && packageUrls != null) {
+ packageUrlsList = Arrays.stream(packageUrls.split(",")).map(String::trim).collect(Collectors.toList());
+ }
+ return packageUrlsList;
+ }
+
+ public void setPackageUrlsList(List 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;
+
+ @JsonAlias("valuesets")
+ @JsonProperty(required = true)
+ List 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 getCodeSystems() {
+ return codeSystems;
+ }
+
+ public void setCodeSystems(List codeSystems) {
+ this.codeSystems = codeSystems;
+ }
+
+ public List getValueSets() {
+ return valueSets;
+ }
+
+ public void setValueSets(List 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 profiles;
+ @JsonProperty
+ String clinicalFocus;
+ @JsonProperty
+ String dataElementScope;
+ @JsonProperty
+ String inclusionCriteria;
+ @JsonProperty
+ String exclusionCriteria;
+ @JsonProperty
+ String usageWarning;
+ @JsonProperty
+ List knowledgeCapability;
+ @JsonProperty
+ List 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 getProfiles() {
+ return profiles;
+ }
+
+ public void setProfiles(List 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 getKnowledgeCapability() {
+ return knowledgeCapability;
+ }
+
+ public void setKnowledgeCapability(List knowledgeCapability) {
+ this.knowledgeCapability = knowledgeCapability;
+ }
+
+ public List getKnowledgeRepresentationLevel() {
+ return knowledgeRepresentationLevel;
+ }
+
+ public void setKnowledgeRepresentationLevel(List 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 input;
+ @JsonProperty
+ List includeFilter;
+ @JsonProperty
+ List 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 getInput() {
+ return input;
+ }
+
+ public void setInput(List input) {
+ this.input = input;
+ }
+
+ public List getIncludeFilter() {
+ return includeFilter;
+ }
+
+ public void setIncludeFilter(List includeFilter) {
+ this.includeFilter = includeFilter;
+ }
+
+ public List getExcludeFilter() {
+ return excludeFilter;
+ }
+
+ public void setExcludeFilter(List 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 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 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 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 getCodesFromCompose(ValueSet vs) {
+ List 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 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 contact;
+ private List 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 getContact() {
+ return contact;
+ }
+
+ public List 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 resourceDirs = params.resourceDirs;
+ List 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 resourcePaths) {
+ public static void ensure(String igPath, Boolean includePatientScenarios, Boolean includeTerminology, List 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> testCasesBundles =
- BundleUtils.GetBundlesInDir(testArtifact.getPath(), fhirContext, false);
+ BundleUtils.getBundlesInDir(testArtifact.getPath(), fhirContext, false);
for (Map.Entry testCaseBundleMapEntry : testCasesBundles) {
IBaseResource testCaseBundle = testCaseBundleMapEntry.getValue();
@@ -284,7 +284,7 @@ private Map.Entry getContentBundleForTestArtifact(String
String contentBundlePath = getPathForContentBundleTestArtifact(groupName, testArtifactName);
File testArtifactContentBundleDirectory = new File(contentBundlePath);
if (testArtifactContentBundleDirectory != null && testArtifactContentBundleDirectory.exists()) {
- List> testArtifactContentBundles = BundleUtils.GetBundlesInDir(contentBundlePath, fhirContext, false);
+ List> 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> resources = BundleUtils.GetBundlesInDir(params.directoryPath, fhirContext);
+ List> 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 resources, FhirContext fhirContext, Boolean addBundleTimestamp, List | | |