From f03274a76168d87f91109d1f40cb1d3c797631cd Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Wed, 13 Dec 2023 20:01:09 -0500 Subject: [PATCH 01/20] Refresh IG POST expanded to include other resources such as group files. Duplicate bundle files during refresh fixed. -x added as possible argument during Refresh IG to show expanded reporting for errors, warnings, and information messages. Bundle Test Cases enhanced with multithreading optimizations. General enhancements to message output for readability. --- .../org/opencds/cqf/tooling/cli/Main.java | 4 + .../cqf/tooling/common/ThreadUtils.java | 41 +- .../cql/exception/CQLTranslatorException.java | 38 -- .../cql/exception/CqlTranslatorException.java | 46 ++ .../cqf/tooling/library/LibraryProcessor.java | 4 +- .../cqf/tooling/measure/MeasureBundler.java | 108 +++++ .../cqf/tooling/measure/MeasureProcessor.java | 130 ++---- .../operation/BundleTestCasesOperation.java | 4 +- .../tooling/operation/RefreshIGOperation.java | 34 +- .../parameter/RefreshIGParameters.java | 1 + .../parameter/RefreshMeasureParameters.java | 2 + ...rceProcessor.java => AbstractBundler.java} | 227 +++++----- .../cqf/tooling/processor/BaseProcessor.java | 14 +- .../cqf/tooling/processor/CqlProcessor.java | 105 ++++- .../tooling/processor/IGBundleProcessor.java | 46 +- .../cqf/tooling/processor/IGProcessor.java | 155 +++---- .../tooling/processor/IGTestProcessor.java | 160 +++---- .../tooling/processor/IProcessorContext.java | 2 + ...cessor.java => PlanDefinitionBundler.java} | 16 +- .../processor/PostBundlesInDirProcessor.java | 42 +- .../tooling/processor/TestCaseProcessor.java | 315 ++++++++------ .../tooling/processor/ValueSetsProcessor.java | 4 +- .../argument/RefreshIGArgumentProcessor.java | 19 +- ...ocessor.java => QuestionnaireBundler.java} | 18 +- .../cqf/tooling/utilities/BundleUtils.java | 10 - .../tooling/utilities/HttpClientUtils.java | 246 ++++++----- .../cqf/tooling/utilities/IOUtils.java | 64 ++- .../cqf/tooling/utilities/ResourceUtils.java | 203 ++++----- .../operation/RefreshIGOperationTest.java | 393 +++++++++--------- .../tooling/processor/IGProcessorTest.java | 13 +- .../PlanDefinitionProcessorTest.java | 2 +- .../QuestionnaireProcessorTest.java | 10 +- 32 files changed, 1365 insertions(+), 1111 deletions(-) delete mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CQLTranslatorException.java create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CqlTranslatorException.java create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java rename tooling/src/main/java/org/opencds/cqf/tooling/processor/{AbstractResourceProcessor.java => AbstractBundler.java} (66%) rename tooling/src/main/java/org/opencds/cqf/tooling/processor/{PlanDefinitionProcessor.java => PlanDefinitionBundler.java} (72%) rename tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/{QuestionnaireProcessor.java => QuestionnaireBundler.java} (70%) 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 c41c30353..1cfccabfd 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 @@ -230,6 +230,7 @@ //import org.opencds.cqf.tooling.operations.ExecutableOperation; //import org.opencds.cqf.tooling.operations.Operation; //import org.reflections.Reflections; +import org.opencds.cqf.tooling.common.ThreadUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -278,6 +279,9 @@ public class Main { // } public static void main(String[] args) { + //ensure any and all executors are shutdown cleanly when app is shutdown: + Runtime.getRuntime().addShutdownHook(new Thread(ThreadUtils::shutdownRunningExecutors)); + 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."); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java index a652bd4e1..a2cade717 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java @@ -1,6 +1,5 @@ package org.opencds.cqf.tooling.common; -import org.opencds.cqf.tooling.utilities.LogUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +13,9 @@ public class ThreadUtils { protected static final Logger logger = LoggerFactory.getLogger(ThreadUtils.class); + + private static List runningExecutors = new ArrayList<>(); + /** * Executes a list of tasks concurrently using a thread pool. *

@@ -23,17 +25,24 @@ public class ThreadUtils { * * @param tasks A list of Callable tasks to execute concurrently. */ - public static void executeTasks(List> tasks) { - if (tasks == null || tasks.isEmpty()){ + public static void executeTasks(List> tasks, ExecutorService executor) { + if (tasks == null || tasks.isEmpty()) { return; } + runningExecutors.add(executor); + + List> retryTasks = new ArrayList<>(); + //let OS handle threading: - ExecutorService executorService = Executors.newCachedThreadPool();// Submit tasks and obtain futures try { List> futures = new ArrayList<>(); for (Callable task : tasks) { - futures.add(executorService.submit(task)); + try { + futures.add(executor.submit(task)); + } catch (OutOfMemoryError e) { + retryTasks.add(task); + } } // Wait for all tasks to complete @@ -41,14 +50,30 @@ public static void executeTasks(List> tasks) { future.get(); } } catch (Exception e) { - logger.error("ThreadUtils.executeTasks", e); + logger.error("ThreadUtils.executeTasks: ", e); } finally { - executorService.shutdown(); + if (retryTasks.isEmpty()) { + runningExecutors.remove(executor); + executor.shutdown(); + }else{ + executeTasks(retryTasks, executor); + } } } + public static void executeTasks(List> tasks) { + executeTasks(tasks, Executors.newCachedThreadPool()); + } + public static void executeTasks(Queue> callables) { + executeTasks(new ArrayList<>(callables), Executors.newCachedThreadPool()); + } - executeTasks(new ArrayList<>(callables)); + public static void shutdownRunningExecutors() { + if (runningExecutors.isEmpty()) return; + for (ExecutorService es : runningExecutors){ + es.shutdownNow(); + } + runningExecutors = new ArrayList<>(); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CQLTranslatorException.java b/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CQLTranslatorException.java deleted file mode 100644 index 5b2b86135..000000000 --- a/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CQLTranslatorException.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.opencds.cqf.tooling.cql.exception; - -import java.io.Serializable; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Custom exception to pass the list of errors returned by the translator to calling methods. - */ -public class CQLTranslatorException extends Exception implements Serializable { - private static final long serialVersionUID = 20600L; - - /** - * Using Set to avoid duplicate entries. - */ - private final transient Set errors = new HashSet<>(); - - public CQLTranslatorException(Exception e) { - super("CQL Translation Error(s): " + e.getMessage()); - } - - public CQLTranslatorException(List errors) { - super("CQL Translation Error(s)"); - this.errors.addAll(errors); - } - - public CQLTranslatorException(String message) { - super("CQL Translation Error(s): " + message); - } - - public Set getErrors() { - if (errors.isEmpty()) { - errors.add(this.getMessage()); - } - return errors; - } -} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CqlTranslatorException.java b/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CqlTranslatorException.java new file mode 100644 index 000000000..e17b3e76e --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/cql/exception/CqlTranslatorException.java @@ -0,0 +1,46 @@ +package org.opencds.cqf.tooling.cql.exception; + +import org.cqframework.cql.cql2elm.CqlCompilerException; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Custom exception to pass the list of errors returned by the translator to calling methods. + */ +public class CqlTranslatorException extends Exception implements Serializable { + private static final long serialVersionUID = 20600L; + + /** + * Using Set to avoid duplicate entries. + */ + private final transient List errors = new ArrayList<>(); + + public CqlTranslatorException(Exception e) { + super("CQL Translation Error(s): " + e.getMessage()); + } + + public CqlTranslatorException(List errors) { + super("CQL Translation Error(s)"); + this.errors.addAll(errors); + } + + public CqlTranslatorException(List errorsInput, CqlCompilerException.ErrorSeverity errorSeverity) { + super("CQL Translation Error(s)"); + for (String error : errorsInput){ + errors.add(new CqlCompilerException(error, errorSeverity)); + } + } + + public CqlTranslatorException(String message) { + super("CQL Translation Error(s): " + message); + } + + public List getErrors() { + if (errors.isEmpty()) { + errors.add(new CqlCompilerException(this.getMessage())); + } + return errors; + } +} 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 b48a7a584..7f0343480 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 @@ -73,7 +73,7 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin } public List refreshIgLibraryContent(BaseProcessor parentContext, Encoding outputEncoding, String libraryPath, String libraryOutputDirectory, Boolean versioned, FhirContext fhirContext, Boolean shouldApplySoftwareSystemStamp) { - System.out.println("Refreshing libraries..."); + System.out.println("\r\n[Refreshing Libraries]\r\n"); // ArrayList refreshedLibraryNames = new ArrayList(); LibraryProcessor libraryProcessor; @@ -90,7 +90,7 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin } if (libraryPath == null) { - libraryPath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.libraryPathElement); + libraryPath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.LIBRARY_PATH_ELEMENT); } else if (!Utilities.isAbsoluteFileName(libraryPath)) { libraryPath = FilenameUtils.concat(parentContext.getRootDir(), libraryPath); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java new file mode 100644 index 000000000..14342e0f2 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java @@ -0,0 +1,108 @@ +package org.opencds.cqf.tooling.measure; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.tooling.processor.AbstractBundler; +import org.opencds.cqf.tooling.utilities.HttpClientUtils; +import org.opencds.cqf.tooling.utilities.IOUtils; +import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +public class MeasureBundler extends AbstractBundler { + public static final String ResourcePrefix = "measure-"; + protected CopyOnWriteArrayList identifiers; + + public static String getId(String baseId) { + return ResourcePrefix + baseId; + } + @Override + protected String getSourcePath(FhirContext fhirContext, Map.Entry resourceEntry) { + return IOUtils.getMeasurePathMap(fhirContext).get(resourceEntry.getKey()); + } + + @Override + protected Map getResources(FhirContext fhirContext) { + return IOUtils.getMeasures(fhirContext); + } + + @Override + protected String getResourceProcessorType() { + return TYPE_MEASURE; + } + + @Override + protected Set getPaths(FhirContext fhirContext) { + return IOUtils.getMeasurePaths(fhirContext); + } + + //so far only the Measure Bundle process needs to persist extra files: + @Override + protected List persistExtraFiles(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri) { + //persist tests-* before group-* files and make a record of which files were tracked: + List persistedFiles = persistTestFilesWithPriority(bundleDestPath, libraryName, encoding, fhirContext, fhirUri); + persistedFiles.addAll(persistEverythingElse(bundleDestPath, libraryName, encoding, fhirContext, fhirUri, persistedFiles)); + + return persistedFiles; + } + + private List persistTestFilesWithPriority(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri) { + List persistedResources = new ArrayList<>(); + String filesLoc = bundleDestPath + File.separator + libraryName + "-files"; + File directory = new File(filesLoc); + if (directory.exists()) { + File[] filesInDir = directory.listFiles(); + + if (!(filesInDir == null || filesInDir.length == 0)) { + for (File file : filesInDir) { + if (file.getName().toLowerCase().startsWith("tests-")) { + try { + IBaseResource resource = IOUtils.readResource(file.getAbsolutePath(), fhirContext, true); + HttpClientUtils.post(fhirUri, resource, encoding, fhirContext, file.getAbsolutePath(), true); + persistedResources.add(file.getAbsolutePath()); + } catch (Exception e) { + //resource is likely not IBaseResource + logger.error("MeasureBundler.persistTestFilesWithPriority", e); + } + } + } + } + } + return persistedResources; + } + + private List persistEverythingElse(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri, List alreadyPersisted) { + List persistedResources = new ArrayList<>(); + String filesLoc = bundleDestPath + File.separator + libraryName + "-files"; + File directory = new File(filesLoc); + if (directory.exists()) { + + File[] filesInDir = directory.listFiles(); + + if (!(filesInDir == null || filesInDir.length == 0)) { + for (File file : filesInDir) { + //don't post what has already been processed + if (alreadyPersisted.contains(file.getAbsolutePath())) { + continue; + } + if (file.getName().toLowerCase().endsWith(".json") || file.getName().toLowerCase().endsWith(".xml")) { + try { + IBaseResource resource = IOUtils.readResource(file.getAbsolutePath(), fhirContext, true); + HttpClientUtils.post(fhirUri, resource, encoding, fhirContext, file.getAbsolutePath(), false); + persistedResources.add(file.getAbsolutePath()); + } catch (Exception e) { + //resource is likely not IBaseResource + logger.error("persistEverythingElse", e); + } + } + } + } + } + return persistedResources; + } +} diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java index 6b46ae569..bcd72826e 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java @@ -2,42 +2,43 @@ import ca.uhn.fhir.context.FhirContext; import org.apache.commons.io.FilenameUtils; -import org.cqframework.cql.cql2elm.CqlCompilerException; -import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.*; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Measure; import org.opencds.cqf.tooling.measure.r4.R4MeasureProcessor; import org.opencds.cqf.tooling.measure.stu3.STU3MeasureProcessor; import org.opencds.cqf.tooling.parameter.RefreshMeasureParameters; -import org.opencds.cqf.tooling.processor.AbstractResourceProcessor; import org.opencds.cqf.tooling.processor.BaseProcessor; +import org.opencds.cqf.tooling.processor.CqlProcessor; import org.opencds.cqf.tooling.processor.IGProcessor; import org.opencds.cqf.tooling.utilities.*; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; -import java.io.File; -import java.io.FilenameFilter; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; -public class MeasureProcessor extends AbstractResourceProcessor { - public static final String ResourcePrefix = "measure-"; - protected List identifiers; +import java.util.concurrent.CopyOnWriteArrayList; + +public class MeasureProcessor extends BaseProcessor { + public static volatile String ResourcePrefix = "measure-"; + protected volatile List identifiers; public static String getId(String baseId) { return ResourcePrefix + baseId; } - public List refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, Boolean versioned, FhirContext fhirContext, String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp) { - return refreshIgMeasureContent(parentContext, outputEncoding, null, versioned, fhirContext, measureToRefreshPath, shouldApplySoftwareSystemStamp); + + public List refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, Boolean versioned, FhirContext fhirContext, + String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp) { + + return refreshIgMeasureContent(parentContext, outputEncoding, null, versioned, fhirContext, measureToRefreshPath, + shouldApplySoftwareSystemStamp); } - public List refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, String measureOutputDirectory, Boolean versioned, FhirContext fhirContext, String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp) { + public List refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, String measureOutputDirectory, + Boolean versioned, FhirContext fhirContext, String measureToRefreshPath, + Boolean shouldApplySoftwareSystemStamp) { - logger.info("Refreshing measures..."); + System.out.println("\r\n[Refreshing Measures]\r\n"); MeasureProcessor measureProcessor; switch (fhirContext.getVersion().getVersion()) { @@ -52,7 +53,7 @@ public List refreshIgMeasureContent(BaseProcessor parentContext, Encodin "Unknown fhir version: " + fhirContext.getVersion().getVersion().getFhirVersionString()); } - String measurePath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.measurePathElement); + String measurePath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.MEASURE_PATH_ELEMENT); RefreshMeasureParameters params = new RefreshMeasureParameters(); params.measurePath = measurePath; params.parentContext = parentContext; @@ -89,89 +90,38 @@ protected List refreshGeneratedContent(List sourceMeasures) { private List internalRefreshGeneratedContent(List sourceMeasures) { // for each Measure, refresh the measure based on the primary measure library List resources = new ArrayList<>(); - for (Measure measure : sourceMeasures) { - resources.add(refreshGeneratedContent(measure)); - } - return resources; - } - - private Measure refreshGeneratedContent(Measure measure) { MeasureRefreshProcessor processor = new MeasureRefreshProcessor(); LibraryManager libraryManager = getCqlProcessor().getLibraryManager(); CqlTranslatorOptions cqlTranslatorOptions = getCqlProcessor().getCqlTranslatorOptions(); - // Do not attempt to refresh if the measure does not have a library - if (measure.hasLibrary()) { - String libraryUrl = ResourceUtils.getPrimaryLibraryUrl(measure, fhirContext); - VersionedIdentifier primaryLibraryIdentifier = CanonicalUtils.toVersionedIdentifier(libraryUrl); - List errors = new ArrayList(); - CompiledLibrary CompiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors); - boolean hasErrors = false; - if (!errors.isEmpty()) { - for (CqlCompilerException e : errors) { - if (e.getSeverity() == CqlCompilerException.ErrorSeverity.Error) { - hasErrors = true; - } - logMessage(e.getMessage()); - } - } - if (!hasErrors) { - return processor.refreshMeasure(measure, libraryManager, CompiledLibrary, cqlTranslatorOptions.getCqlCompilerOptions()); + for (Measure measure : sourceMeasures) { + // Do not attempt to refresh if the measure does not have a library + if (measure.hasLibrary()) { + resources.add(refreshGeneratedContent(measure, processor, libraryManager, cqlTranslatorOptions)); + } else { + resources.add(measure); } } - return measure; - } - //abstract methods to override: - @Override - protected void persistTestFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { - - String filesLoc = bundleDestPath + File.separator + libraryName + "-files"; - File directory = new File(filesLoc); - if (directory.exists()) { - - File[] filesInDir = directory.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.toLowerCase().startsWith("tests-"); - } - }); - - if (!(filesInDir == null || filesInDir.length == 0)) { - for (File file : filesInDir) { - if (file.getName().toLowerCase().startsWith("tests-")) { - try { - IBaseResource resource = IOUtils.readResource(file.getAbsolutePath(), fhirContext, true); - //ensure the resource can be posted - if (BundleUtils.resourceIsTransactionBundle(resource)) { - BundleUtils.postBundle(encoding, fhirContext, fhirUri, resource); - } - } catch (Exception e) { - //resource is likely not IBaseResource - logger.error("persistTestFiles", e); - } - } - } - } - } + return resources; } - @Override - protected String getSourcePath(FhirContext fhirContext, Map.Entry resourceEntry) { - return IOUtils.getMeasurePathMap(fhirContext).get(resourceEntry.getKey()); - } + private Measure refreshGeneratedContent(Measure measure, MeasureRefreshProcessor processor, LibraryManager libraryManager, CqlTranslatorOptions cqlTranslatorOptions) { - @Override - protected Map getResources(FhirContext fhirContext) { - return IOUtils.getMeasures(fhirContext); - } + String libraryUrl = ResourceUtils.getPrimaryLibraryUrl(measure, fhirContext); + VersionedIdentifier primaryLibraryIdentifier = CanonicalUtils.toVersionedIdentifier(libraryUrl); - @Override - protected String getResourceProcessorType() { - return TYPE_MEASURE; - } + List errors = new CopyOnWriteArrayList<>(); + CompiledLibrary CompiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors); - @Override - protected Set getPaths(FhirContext fhirContext) { - return IOUtils.getMeasurePaths(fhirContext); + System.out.println(CqlProcessor.buildStatusMessage(errors, measure.getName(), includeErrors)); + + boolean hasSevereErrors = CqlProcessor.hasSevereErrors(errors); + + //refresh measures without severe errors: + if (!hasSevereErrors) { + return processor.refreshMeasure(measure, libraryManager, CompiledLibrary, cqlTranslatorOptions.getCqlCompilerOptions()); + } + + return measure; } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleTestCasesOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleTestCasesOperation.java index 721ba4640..7ed959167 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleTestCasesOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleTestCasesOperation.java @@ -25,10 +25,10 @@ public void execute(String[] args) { System.err.println(e.getMessage()); System.exit(1); } - + FhirContext fhirContext = ResourceUtils.getFhirContext(ResourceUtils.FhirVersion.parse(params.igVersion.toString())); TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); - testCaseProcessor.refreshTestCases(params.path, Encoding.JSON, fhirContext); + testCaseProcessor.refreshTestCases(params.path, Encoding.JSON, fhirContext, true); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java index 42cf36e87..ea69ec9a6 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java @@ -1,28 +1,22 @@ package org.opencds.cqf.tooling.operation; import org.opencds.cqf.tooling.Operation; -import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.measure.MeasureProcessor; import org.opencds.cqf.tooling.parameter.RefreshIGParameters; -import org.opencds.cqf.tooling.processor.CDSHooksProcessor; -import org.opencds.cqf.tooling.processor.IGBundleProcessor; import org.opencds.cqf.tooling.processor.IGProcessor; -import org.opencds.cqf.tooling.processor.PlanDefinitionProcessor; import org.opencds.cqf.tooling.processor.argument.RefreshIGArgumentProcessor; -import org.opencds.cqf.tooling.questionnaire.QuestionnaireProcessor; public class RefreshIGOperation extends Operation { - public RefreshIGOperation() { - } + public RefreshIGOperation() { + } @Override public void execute(String[] args) { - - if (args == null) { - throw new IllegalArgumentException(); - } - + + if (args == null) { + throw new IllegalArgumentException(); + } + RefreshIGParameters params = null; try { params = new RefreshIGArgumentProcessor().parseAndConvert(args); @@ -31,14 +25,12 @@ public void execute(String[] args) { System.err.println(e.getMessage()); System.exit(1); } - MeasureProcessor measureProcessor = new MeasureProcessor(); - LibraryProcessor libraryProcessor = new LibraryProcessor(); - CDSHooksProcessor cdsHooksProcessor = new CDSHooksProcessor(); - PlanDefinitionProcessor planDefinitionProcessor = new PlanDefinitionProcessor(libraryProcessor, cdsHooksProcessor); - QuestionnaireProcessor questionnaireProcessor = new QuestionnaireProcessor(libraryProcessor); - IGBundleProcessor igBundleProcessor = new IGBundleProcessor(measureProcessor, planDefinitionProcessor, questionnaireProcessor); - IGProcessor processor = new IGProcessor(igBundleProcessor, libraryProcessor, measureProcessor); - processor.publishIG(params); + + if (params.includeErrors == null || !params.includeErrors) { + System.out.println("\r\nRe-run with -x to for expanded reporting of errors, warnings, and informational messages.\r\n"); + } + + new IGProcessor().publishIG(params); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java index a264350aa..d65f61ca8 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java @@ -24,4 +24,5 @@ public class RefreshIGParameters { public String libraryPath; public String libraryOutputPath; public String measureOutputPath; + public Boolean includeErrors; } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java index 28d2c8ec7..624191b08 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java @@ -50,4 +50,6 @@ The path to the measure resource(s) Directory target for writing output */ public String measureOutputDirectory; + + public Boolean includeErrors; } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractResourceProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java similarity index 66% rename from tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractResourceProcessor.java rename to tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index e674b7909..a7934bf7a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractResourceProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -2,32 +2,33 @@ import ca.uhn.fhir.context.FhirContext; import org.apache.commons.io.FilenameUtils; +import org.cqframework.cql.cql2elm.CqlCompilerException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.tooling.common.ThreadUtils; -import org.opencds.cqf.tooling.cql.exception.CQLTranslatorException; +import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.utilities.BundleUtils; -import org.opencds.cqf.tooling.utilities.IOUtils; -import org.opencds.cqf.tooling.utilities.LogUtils; -import org.opencds.cqf.tooling.utilities.ResourceUtils; +import org.opencds.cqf.tooling.utilities.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.io.IOException; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; /** - * An abstract base class for processors that handle the bundling of various types of resources within an ig. + * An abstract base class for bundlers that handle the bundling of various types of resources within an ig. * This class provides methods for bundling resources, including dependencies and test cases, and handles the execution of associated tasks. * Subclasses must implement specific methods for gathering, processing, and persisting resources. */ -public abstract class AbstractResourceProcessor extends BaseProcessor { +public abstract class AbstractBundler { + public static final String separator = System.getProperty("file.separator"); + public static final String NEWLINE_INDENT2 = "\n\t\t"; + public static final String NEWLINE_INDENT = "\r\n\t"; + public static final String INDENT = "\t"; + public static final String NEWLINE = "\r\n"; /** * The logger for logging messages specific to the implementing class. */ @@ -76,42 +77,57 @@ protected List getIdentifiers() { return identifiers; } + private String getResourcePrefix() { + return getResourceProcessorType().toLowerCase() + "-"; + } + + protected abstract Set getPaths(FhirContext fhirContext); + + protected abstract String getSourcePath(FhirContext fhirContext, Map.Entry resourceEntry); + + /** + * Handled by the child class in gathering specific IBaseResources + */ + protected abstract Map getResources(FhirContext fhirContext); + + protected abstract String getResourceProcessorType(); + + /** * Bundles resources within an Implementation Guide based on specified options. * - * @param refreshedLibraryNames A list of refreshed library names. - * @param igPath The path to the IG. - * @param binaryPaths The list of binary paths. - * @param includeDependencies Flag indicating whether to include dependencies. - * @param includeTerminology Flag indicating whether to include terminology. + * @param refreshedLibraryNames A list of refreshed library names. + * @param igPath The path to the IG. + * @param binaryPaths The list of binary paths. + * @param includeDependencies Flag indicating whether to include dependencies. + * @param includeTerminology Flag indicating whether to include terminology. * @param includePatientScenarios Flag indicating whether to include patient scenarios. - * @param includeVersion Flag indicating whether to include version information. - * @param addBundleTimestamp Flag indicating whether to add a timestamp to the bundle. - * @param fhirContext The FHIR context. - * @param fhirUri The FHIR server URI. - * @param encoding The encoding type for processing resources. + * @param includeVersion Flag indicating whether to include version information. + * @param addBundleTimestamp Flag indicating whether to add a timestamp to the bundle. + * @param fhirContext The FHIR context. + * @param fhirUri The FHIR server URI. + * @param encoding The encoding type for processing resources. */ public void bundleResources(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean includeVersion, Boolean addBundleTimestamp, - FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding) { + FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean includeErrors) { - Map resourcesMap = getResources(fhirContext); - List bundledResources = new CopyOnWriteArrayList<>(); + final Map resourcesMap = getResources(fhirContext); + final List bundledResources = new CopyOnWriteArrayList<>(); //for keeping track of progress: - List processedResources = new CopyOnWriteArrayList<>(); + final List processedResources = new CopyOnWriteArrayList<>(); //for keeping track of failed reasons: - Map failedExceptionMessages = new ConcurrentHashMap<>(); + final Map failedExceptionMessages = new ConcurrentHashMap<>(); - Map> translatorWarningMessages = new ConcurrentHashMap<>(); - - int totalResources = resourcesMap.size(); + final Map> cqlTranslatorErrorMessages = new ConcurrentHashMap<>(); //build list of tasks via for loop: List> tasks = new ArrayList<>(); try { - final StringBuilder bundleTestFileStringBuilder = new StringBuilder(); + + final StringBuilder persistedFileReport = new StringBuilder(); final Map libraryUrlMap = IOUtils.getLibraryUrlMap(fhirContext); final Map libraries = IOUtils.getLibraries(fhirContext); final Map libraryPathMap = IOUtils.getLibraryPathMap(fhirContext); @@ -129,13 +145,14 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa //no path for this resource: if (resourceEntry.getKey() == null || resourceEntry.getKey().equalsIgnoreCase("null")) { - if (!resourceId.isEmpty()) { + if (resourceId != null && !resourceId.isEmpty()) { failedExceptionMessages.put(resourceId, "Path is null for " + resourceId); } continue; } final String resourceSourcePath = getSourcePath(fhirContext, resourceEntry); + tasks.add(() -> { //check if resourceSourcePath has been processed before: if (processedResources.contains(resourceSourcePath)) { @@ -185,16 +202,11 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } if (includeTerminology) { - //throws CQLTranslatorException if failed, which will be logged and reported it in the final summary + //throws CQLTranslatorException if failed with severe errors, which will be logged and reported it in the final summary try { ValueSetsProcessor.bundleValueSets(cqlLibrarySourcePath, igPath, fhirContext, resources, encoding, includeDependencies, includeVersion); - }catch (CQLTranslatorException warn){ - if (translatorWarningMessages.containsKey(primaryLibraryName)){ - Set existingMessages = translatorWarningMessages.get(primaryLibraryName); - existingMessages.addAll(warn.getErrors()); - }else{ - translatorWarningMessages.put(primaryLibraryName, warn.getErrors()); - } + } catch (CqlTranslatorException cqlTranslatorException) { + cqlTranslatorErrorMessages.put(primaryLibraryName, cqlTranslatorException.getErrors()); } } @@ -221,43 +233,47 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } if (shouldPersist) { - String bundleDestPath = FilenameUtils.concat(FilenameUtils.concat(IGProcessor.getBundlesPath(igPath), getResourceTestGroupName()), resourceName); - persistBundle(igPath, bundleDestPath, resourceName, encoding, fhirContext, new ArrayList(resources.values()), fhirUri, addBundleTimestamp); + persistBundle(bundleDestPath, resourceName, encoding, fhirContext, new ArrayList(resources.values()), fhirUri, addBundleTimestamp); - String possibleBundleTestMessage = bundleFiles(igPath, bundleDestPath, resourceName, binaryPaths, resourceSourcePath, + bundleFiles(igPath, bundleDestPath, resourceName, binaryPaths, resourceSourcePath, primaryLibrarySourcePath, fhirContext, encoding, includeTerminology, includeDependencies, includePatientScenarios, - includeVersion, addBundleTimestamp, translatorWarningMessages); + includeVersion, addBundleTimestamp, cqlTranslatorErrorMessages); - //Check for test files in bundleDestPath + "-files", loop through if exists, - // find all files that start with "tests-", post to fhir server following same folder structure: - persistTestFiles(bundleDestPath, resourceName, encoding, fhirContext, fhirUri); + //Child classes implement any extra processing of files (Such as MeasureBundler persisting tests-*) + List persistedExtraFiles = persistExtraFiles(bundleDestPath, resourceName, encoding, fhirContext, fhirUri); if (cdsHooksProcessor != null) { List activityDefinitionPaths = CDSHooksProcessor.bundleActivityDefinitions(resourceSourcePath, fhirContext, resources, encoding, includeVersion, shouldPersist); cdsHooksProcessor.addActivityDefinitionFilesToBundle(igPath, bundleDestPath, activityDefinitionPaths, fhirContext, encoding); } - if (!possibleBundleTestMessage.isEmpty()) { - bundleTestFileStringBuilder.append(possibleBundleTestMessage); + //If user supplied a fhir server url, inform them of total # of files to be persisted to the server: + if (fhirUri != null && !fhirUri.isEmpty()) { + persistedFileReport.append("\r\n") + //all persisted files + the bundle: + .append(persistedExtraFiles.size() + 1) + .append(" total files will be posted to ") + .append(fhirUri) + .append(" for ") + .append(resourceName); } - bundledResources.add(resourceSourcePath); } } catch (Exception e) { - failedExceptionMessages.put(resourceSourcePath, e.getMessage()); + if (resourceSourcePath == null) { + failedExceptionMessages.put(resourceEntry.getValue().getIdElement().getIdPart(), e.getMessage()); + } else { + failedExceptionMessages.put(resourceSourcePath, e.getMessage()); + } } - processedResources.add(resourceSourcePath); + reportProgress(processedResources.size(), tasks.size()); - synchronized (this) { - double percentage = (double) processedResources.size() / totalResources * 100; - System.out.println("Bundle " + getResourceProcessorType() + "s Progress: " + String.format("%.2f%%", percentage) + " PROCESSED: " + resourceEntry.getKey()); - } //task requires return statement return null; }); @@ -266,10 +282,8 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa ThreadUtils.executeTasks(tasks); - //Test file information: - String bundleTestFileMessage = bundleTestFileStringBuilder.toString(); - if (!bundleTestFileMessage.isEmpty()) { - System.out.println(bundleTestFileMessage); + if (!persistedFileReport.toString().isEmpty()) { + System.out.println(persistedFileReport); } @@ -278,9 +292,9 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } - StringBuilder message = new StringBuilder("\r\n" + bundledResources.size() + " " + getResourceProcessorType() + "(s) successfully bundled:"); + StringBuilder message = new StringBuilder(NEWLINE + bundledResources.size() + " " + getResourceProcessorType() + "(s) successfully bundled:"); for (String bundledResource : bundledResources) { - message.append("\r\n ").append(bundledResource).append(" BUNDLED"); + message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); } List resourcePathLibraryNames = new ArrayList<>(getPaths(fhirContext)); @@ -290,44 +304,67 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa resourcePathLibraryNames.removeAll(bundledResources); resourcePathLibraryNames.retainAll(refreshedLibraryNames); - message.append("\r\n").append(resourcePathLibraryNames.size()).append(" ").append(getResourceProcessorType()).append("(s) refreshed, but not bundled (due to issues):"); + message.append(NEWLINE).append(resourcePathLibraryNames.size()).append(" ").append(getResourceProcessorType()).append("(s) refreshed, but not bundled (due to issues):"); for (String notBundled : resourcePathLibraryNames) { - message.append("\r\n ").append(notBundled).append(" REFRESHED"); + message.append(NEWLINE_INDENT).append(notBundled).append(" REFRESHED"); } //attempt to give some kind of informational message: failedResources.removeAll(bundledResources); failedResources.removeAll(resourcePathLibraryNames); - message.append("\r\n").append(failedResources.size()).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); + message.append(NEWLINE).append(failedResources.size()).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); for (String failed : failedResources) { if (failedExceptionMessages.containsKey(failed)) { - message.append("\r\n ").append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); + message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); } else { - message.append("\r\n ").append(failed).append(" FAILED"); + message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); } } //Exceptions stemming from IOUtils.translate that did not prevent process from completing for file: - if (!translatorWarningMessages.isEmpty()) { - message.append("\r\n").append(translatorWarningMessages.size()).append(" ").append(getResourceProcessorType()).append("(s) encountered warnings:"); - for (String library : translatorWarningMessages.keySet()) { - message.append("\r\n ").append(library).append(" WARNING: ").append(translatorWarningMessages.get(library)); + if (!cqlTranslatorErrorMessages.isEmpty()) { + message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceProcessorType()).append("(s) encountered CQL translator errors:"); + + for (String library : cqlTranslatorErrorMessages.keySet()) { + message.append(INDENT).append( + CqlProcessor.buildStatusMessage(cqlTranslatorErrorMessages.get(library), library, includeErrors, false, NEWLINE_INDENT2) + ).append(NEWLINE); } } System.out.println(message.toString()); } - private String getResourcePrefix() { - return getResourceProcessorType().toLowerCase() + "-"; + + private void reportProgress(int count, int total) { + double percentage = (double) count / total * 100; + System.out.print("\rBundle " + getResourceProcessorType() + "s: " + String.format("%.2f%%", percentage) + " processed."); } - protected abstract Set getPaths(FhirContext fhirContext); + private String getResourceTestGroupName() { + return getResourceProcessorType().toLowerCase(); + } + + private void persistBundle(String bundleDestPath, String libraryName, + IOUtils.Encoding encoding, FhirContext fhirContext, + List resources, String fhirUri, + Boolean addBundleTimestamp) throws IOException { + IOUtils.initializeDirectory(bundleDestPath); + Object bundle = BundleUtils.bundleArtifacts(libraryName, resources, fhirContext, addBundleTimestamp, this.getIdentifiers()); + IOUtils.writeBundle(bundle, bundleDestPath, encoding, fhirContext); + + if (fhirUri != null && !fhirUri.isEmpty()) { + String resourceWriteLocation = bundleDestPath + separator + libraryName + "-bundle." + encoding; + HttpClientUtils.post(fhirUri, (IBaseResource) bundle, encoding, fhirContext, resourceWriteLocation, true); + } + } - private String bundleFiles(String igPath, String bundleDestPath, String primaryLibraryName, List binaryPaths, String resourceFocusSourcePath, - String librarySourcePath, FhirContext fhirContext, IOUtils.Encoding encoding, Boolean includeTerminology, Boolean includeDependencies, Boolean includePatientScenarios, - Boolean includeVersion, Boolean addBundleTimestamp, Map> translatorWarningMessages) { - String bundleMessage = ""; + + protected abstract List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri); + + private void bundleFiles(String igPath, String bundleDestPath, String primaryLibraryName, List binaryPaths, String resourceFocusSourcePath, + String librarySourcePath, FhirContext fhirContext, IOUtils.Encoding encoding, Boolean includeTerminology, Boolean includeDependencies, Boolean includePatientScenarios, + Boolean includeVersion, Boolean addBundleTimestamp, Map> translatorWarningMessages) { String bundleDestFilesPath = FilenameUtils.concat(bundleDestPath, primaryLibraryName + "-" + IGBundleProcessor.bundleFilesPathElement); IOUtils.initializeDirectory(bundleDestFilesPath); @@ -350,13 +387,8 @@ private String bundleFiles(String igPath, String bundleDestPath, String primaryL Object bundle = BundleUtils.bundleArtifacts(ValueSetsProcessor.getId(primaryLibraryName), new ArrayList(valueSets.values()), fhirContext, addBundleTimestamp, this.getIdentifiers()); IOUtils.writeBundle(bundle, bundleDestFilesPath, encoding, fhirContext); } - }catch (CQLTranslatorException warn){ - if (translatorWarningMessages.containsKey(primaryLibraryName)){ - Set existingMessages = translatorWarningMessages.get(primaryLibraryName); - existingMessages.addAll(warn.getErrors()); - }else{ - translatorWarningMessages.put(primaryLibraryName, warn.getErrors()); - } + } catch (CqlTranslatorException cqlTranslatorException) { + translatorWarningMessages.put(primaryLibraryName, cqlTranslatorException.getErrors()); } } @@ -370,34 +402,9 @@ private String bundleFiles(String igPath, String bundleDestPath, String primaryL } if (includePatientScenarios) { - bundleMessage = TestCaseProcessor.bundleTestCaseFiles(igPath, getResourceTestGroupName(), primaryLibraryName, bundleDestFilesPath, fhirContext); + TestCaseProcessor.bundleTestCaseFiles(igPath, getResourceTestGroupName(), primaryLibraryName, bundleDestFilesPath, fhirContext); } - return bundleMessage; - } - - - private void persistBundle(String igPath, String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, List resources, String fhirUri, Boolean addBundleTimestamp) { - IOUtils.initializeDirectory(bundleDestPath); - Object bundle = BundleUtils.bundleArtifacts(libraryName, resources, fhirContext, addBundleTimestamp, this.getIdentifiers()); - IOUtils.writeBundle(bundle, bundleDestPath, encoding, fhirContext); - - BundleUtils.postBundle(encoding, fhirContext, fhirUri, (IBaseResource) bundle); - } - - protected abstract void persistTestFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri); - - protected abstract String getSourcePath(FhirContext fhirContext, Map.Entry resourceEntry); - - /** - * Handled by the child class in gathering specific IBaseResources - */ - protected abstract Map getResources(FhirContext fhirContext); - - protected abstract String getResourceProcessorType(); - - private String getResourceTestGroupName() { - return getResourceProcessorType().toLowerCase(); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java index 3f0aca3a6..8d3861c43 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.fhir.ucum.UcumEssenceService; import org.fhir.ucum.UcumException; @@ -74,6 +75,13 @@ public NpmPackageManager getPackageManager() { protected IProcessorContext parentContext; + //used to inform user of errors that occurred during refresh of library, measure, or test cases + public Boolean includeErrors = false; + + public Boolean getIncludeErrors() { + return includeErrors; + } + public void initialize(IProcessorContext context) { this.parentContext = context; @@ -87,6 +95,7 @@ public void initialize(IProcessorContext context) { this.packageManager = parentContext.getPackageManager(); this.binaryPaths = parentContext.getBinaryPaths(); this.cqlProcessor = parentContext.getCqlProcessor(); + this.includeErrors = parentContext.getIncludeErrors(); } } @@ -159,8 +168,9 @@ public CqlProcessor getCqlProcessor() { if (packageManager == null) { throw new IllegalStateException("packageManager is null. It should be initialized at this point."); } - cqlProcessor = new CqlProcessor(packageManager.getNpmList(), binaryPaths, reader, this, ucumService, - packageId, canonicalBase); + cqlProcessor = new CqlProcessor(new CopyOnWriteArrayList<>(packageManager.getNpmList()), + new CopyOnWriteArrayList<>(binaryPaths), reader, this, ucumService, + packageId, canonicalBase, includeErrors); } return cqlProcessor; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java index 00f2a2047..ccb56f9ff 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java @@ -3,11 +3,8 @@ import java.io.File; import java.io.FilenameFilter; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; import org.cqframework.cql.cql2elm.CqlCompilerException; import org.cqframework.cql.cql2elm.CqlCompilerOptions; @@ -138,7 +135,9 @@ public List getParameters() { private NamespaceInfo namespaceInfo; - public CqlProcessor(List packages, List folders, ILibraryReader reader, ILoggingService logger, UcumService ucumService, String packageId, String canonicalBase) { + private boolean includeErrors; + + public CqlProcessor(List packages, List folders, ILibraryReader reader, ILoggingService logger, UcumService ucumService, String packageId, String canonicalBase, Boolean includeErrors) { super(); this.packages = packages; this.folders = folders; @@ -150,6 +149,7 @@ public CqlProcessor(List packages, List folders, ILibraryRea if (packageId != null && !packageId.isEmpty() && canonicalBase != null && !canonicalBase.isEmpty()) { this.namespaceInfo = new NamespaceInfo(packageId, canonicalBase); } + this.includeErrors = includeErrors; } /** @@ -161,7 +161,6 @@ public CqlProcessor(List packages, List folders, ILibraryRea */ public void execute() throws FHIRException { try { - logger.logMessage("Translating CQL source"); fileMap = new HashMap<>(); // foreach folder @@ -250,7 +249,7 @@ public LibraryManager getLibraryManager() { } private void translateFolder(String folder) { - logger.logMessage(String.format("Translating CQL source in folder %s", folder)); + System.out.printf("Translating CQL source in folder %s%n", folder); CqlTranslatorOptions options = ResourceUtils.getTranslatorOptions(folder); @@ -343,7 +342,7 @@ public static ValidationMessage exceptionToValidationMessage(File file, CqlCompi } private void translateFile(LibraryManager libraryManager, File file, CqlCompilerOptions options) { - logger.logMessage(String.format("Translating CQL source in file %s", file.toString())); +// logger.logMessage(String.format("Translating CQL source in file %s", file.toString())); CqlSourceFileInformation result = new CqlSourceFileInformation(); fileMap.put(file.getAbsoluteFile().toString(), result); @@ -361,14 +360,12 @@ private void translateFile(LibraryManager libraryManager, File file, CqlCompiler result.getErrors().add(exceptionToValidationMessage(file, exception)); } - if (!translator.getErrors().isEmpty()) { + List severeErrorList = listBySeverity(translator.getErrors(), CqlCompilerException.ErrorSeverity.Error); + + + if (!severeErrorList.isEmpty()) { result.getErrors().add(new ValidationMessage(ValidationMessage.Source.Publisher, IssueType.EXCEPTION, file.getName(), String.format("CQL Processing failed with (%d) errors.", translator.getErrors().size()), IssueSeverity.ERROR)); - logger.logMessage(String.format("Translation failed with (%d) errors; see the error log for more information.", translator.getErrors().size())); - - for (CqlCompilerException error : translator.getErrors()) { - logger.logMessage(String.format("Error: %s", error.getMessage())); - } } else { try { @@ -406,15 +403,19 @@ private void translateFile(LibraryManager libraryManager, File file, CqlCompiler // Extract dataRequirement data result.dataRequirements.addAll(requirementsLibrary.getDataRequirement()); - logger.logMessage("CQL translation completed successfully."); } catch (Exception ex) { logger.logMessage(String.format("CQL Translation succeeded for file: '%s', but ELM generation failed with the following error: %s", file.getAbsolutePath(), ex.getMessage())); } } + + //output Success/Warn/Info/Fail message to user: + System.out.println(buildStatusMessage(translator.getErrors(), file.getName(), includeErrors)); } catch (Exception e) { result.getErrors().add(new ValidationMessage(ValidationMessage.Source.Publisher, IssueType.EXCEPTION, file.getName(), "CQL Processing failed with exception: "+e.getMessage(), IssueSeverity.ERROR)); } + + } private FilenameFilter getCqlFilenameFilter() { @@ -425,4 +426,74 @@ public boolean accept(File path, String name) { } }; } -} + + + private static List listTranslatorErrors(List translatorErrors) { + List errors = new ArrayList<>(); + for (CqlCompilerException error : translatorErrors) { + errors.add(error.getSeverity().toString() + ": " + + (error.getLocator() == null ? "" : String.format("[%d:%d, %d:%d] ", + error.getLocator().getStartLine(), + error.getLocator().getStartChar(), + error.getLocator().getEndLine(), + error.getLocator().getEndChar())) + + error.getMessage().replace("\n", "").replace("\r", "")); + } + Collections.sort(errors); + return errors; + } + + private static List listBySeverity(List errors, CqlCompilerException.ErrorSeverity errorSeverity) { + return errors.stream() + .filter(exception -> exception.getSeverity() == errorSeverity) + .collect(Collectors.toList()); + } + + public static String buildStatusMessage(List errors, String resourceName, boolean includeErrors){ + return buildStatusMessage(errors, resourceName, includeErrors, true, "\n\t"); + } + + public static String buildStatusMessage(List errors, String resourceName, boolean includeErrors, boolean withStatusIndicator, String delimiter){ + String successMsg = "[SUCCESS] CQL Processing of "; + String statusIndicatorMinor = " completed successfully"; + String statusIndicator; + + //empty list means no errors, so success + if (errors == null || errors.isEmpty()){ + return successMsg + resourceName + statusIndicatorMinor; + } + + //separate out exceptions by their severity to determine the messaging to the user: + List infosList = listBySeverity(errors, CqlCompilerException.ErrorSeverity.Info); + List warningsList = listBySeverity(errors, CqlCompilerException.ErrorSeverity.Warning); + List errorList = listBySeverity(errors, CqlCompilerException.ErrorSeverity.Error); + + if (!errorList.isEmpty()) { + statusIndicator = "[FAIL] "; + statusIndicatorMinor = " failed"; + } else if (!warningsList.isEmpty()) { + statusIndicator = "[WARN] "; + } else if (!infosList.isEmpty()) { + statusIndicator = "[INFO] "; + } else { + return successMsg + resourceName + statusIndicatorMinor; + } + List fullSortedList = new ArrayList<>(); + fullSortedList.addAll(CqlProcessor.listTranslatorErrors(infosList)); + fullSortedList.addAll(CqlProcessor.listTranslatorErrors(warningsList)); + fullSortedList.addAll(CqlProcessor.listTranslatorErrors(errorList)); + Collections.sort(fullSortedList); + String fullSortedListMsg = String.join(delimiter, fullSortedList); + + String errorsStatus = errorList.size() + " Error(s)" ; + String infoStatus = infosList.size() + " Information Message(s)" ; + String warningStatus = warningsList.size() + " Warning(s)" ; + + return (withStatusIndicator ? statusIndicator : "") + "CQL Processing of " + resourceName + statusIndicatorMinor + " with " + errorsStatus + ", " + + warningStatus + ", and " + infoStatus + (includeErrors ? ": " + delimiter + fullSortedListMsg : ""); + } + + public static boolean hasSevereErrors(List errors) { + return errors.stream().anyMatch(error -> error.getSeverity() == CqlCompilerException.ErrorSeverity.Error); + } +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java index 8a4eca09a..7432f8f70 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java @@ -1,12 +1,12 @@ package org.opencds.cqf.tooling.processor; import ca.uhn.fhir.context.FhirContext; -import org.opencds.cqf.tooling.measure.MeasureProcessor; -import org.opencds.cqf.tooling.questionnaire.QuestionnaireProcessor; +import org.opencds.cqf.tooling.library.LibraryProcessor; +import org.opencds.cqf.tooling.measure.MeasureBundler; +import org.opencds.cqf.tooling.questionnaire.QuestionnaireBundler; import org.opencds.cqf.tooling.utilities.HttpClientUtils; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; -import org.opencds.cqf.tooling.utilities.LogUtils; import org.opencds.cqf.tooling.utilities.ResourceUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,14 +19,15 @@ public class IGBundleProcessor { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String bundleFilesPathElement = "files/"; - MeasureProcessor measureProcessor; - PlanDefinitionProcessor planDefinitionProcessor; - QuestionnaireProcessor questionnaireProcessor; - - public IGBundleProcessor(MeasureProcessor measureProcessor, PlanDefinitionProcessor planDefinitionProcessor, QuestionnaireProcessor questionnaireProcessor) { - this.measureProcessor = measureProcessor; - this.planDefinitionProcessor = planDefinitionProcessor; - this.questionnaireProcessor = questionnaireProcessor; + + private Boolean includeErrors = true; + LibraryProcessor libraryProcessor; + CDSHooksProcessor cdsHooksProcessor; + + public IGBundleProcessor(Boolean includeErrors, LibraryProcessor libraryProcessor, CDSHooksProcessor cdsHooksProcessor) { + this.includeErrors = includeErrors; + this.libraryProcessor = libraryProcessor; + this.cdsHooksProcessor = cdsHooksProcessor; } public void bundleIg(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Encoding encoding, Boolean includeELM, @@ -34,36 +35,36 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis FhirContext fhirContext, String fhirUri) { System.out.println("\n"); - + System.out.println("\r\n[Bundle Measures has started - " + getTime() + "]\r\n"); - measureProcessor.bundleResources(refreshedLibraryNames, + new MeasureBundler().bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding); + fhirUri, encoding, includeErrors); //this message can be moved to any point of this process, but so far it's just the bundle measure process //that will persist test files. If Questionnaires and PlanDefinitions should ever need test files as well //persistTestFiles can be moved to AbstractResourceProcessor from MeasureProcessor instead of abstract sig - System.out.println("\r\nTotal \"tests-*\" files copied: " + IOUtils.copyFileCounter() + ". " + + System.out.println("\r\nTotal test files copied: " + IOUtils.copyFileCounter() + ". " + (fhirUri != null && !fhirUri.isEmpty() ? "These files will be posted to " + fhirUri : "") ); System.out.println("\r\n[Bundle Measures has finished - " + getTime() + "]\r\n"); - + System.out.println("\r\n[Bundle PlanDefinitions has started - " + getTime() + "]\r\n"); - planDefinitionProcessor.bundleResources(refreshedLibraryNames, + new PlanDefinitionBundler(this.libraryProcessor, this.cdsHooksProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding); + fhirUri, encoding, includeErrors); System.out.println("\r\n[Bundle PlanDefinitions has finished - " + getTime() + "]\r\n"); - - + + System.out.println("\r\n[Bundle Questionnaires has started - " + getTime() + "]\r\n"); - questionnaireProcessor.bundleResources(refreshedLibraryNames, + new QuestionnaireBundler(this.libraryProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding); + fhirUri, encoding, includeErrors); System.out.println("\r\n[Bundle Questionnaires has finished - " + getTime() + "]\r\n"); @@ -78,7 +79,6 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis // run cleanup (maven runs all ci tests sequentially and static member variables could retain values from previous tests) IOUtils.cleanUp(); ResourceUtils.cleanUp(); - TestCaseProcessor.cleanUp(); } private String getTime() { 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 8941eec18..82afc58b3 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 @@ -24,19 +24,16 @@ import org.slf4j.LoggerFactory; public class IGProcessor extends BaseProcessor { - private static final Logger logger = LoggerFactory.getLogger(IGProcessor.class); - public static final String IG_VERSION_REQUIRED = "igVersion required"; - protected IGBundleProcessor igBundleProcessor; - protected LibraryProcessor libraryProcessor; - protected MeasureProcessor measureProcessor; - - public IGProcessor(IGBundleProcessor igBundleProcessor, LibraryProcessor libraryProcessor, MeasureProcessor measureProcessor) { - this.libraryProcessor = libraryProcessor; - this.measureProcessor = measureProcessor; - this.igBundleProcessor = igBundleProcessor; - } + public static final String CQL_LIBRARY_PATH_ELEMENT = "input/pagecontent/cql/"; + public static final String LIBRARY_PATH_ELEMENT = "input/resources/library/"; + public static final String MEASURE_PATH_ELEMENT = "input/resources/measure/"; + public static final String PLAN_DEFINITION_PATH_ELEMENT = "input/resources/plandefinition/"; + public static final String VALUE_SETS_PATH_ELEMENT = "input/vocabulary/valueset/"; + public static final String TEST_CASE_PATH_ELEMENT = "input/tests/"; + public static final String DEVICE_PATH_ELEMENT = "input/resources/device/"; + //mega ig method public void publishIG(RefreshIGParameters params) { if (params.skipPackages == null) { @@ -54,26 +51,19 @@ public void publishIG(RefreshIGParameters params) { boolean rootDirProvided = params.rootDir != null && !params.rootDir.isEmpty(); boolean igPathProvided = params.igPath != null && !params.igPath.isEmpty(); + //presence of -x arg means give error details instead of just error count during cql processing + includeErrors = (params.includeErrors != null ? params.includeErrors : false); + if (!iniProvided && (!rootDirProvided || !igPathProvided)) { throw new IllegalArgumentException("Either the ini argument or both igPath and rootDir must be provided"); } if (params.ini != null) { initializeFromIni(params.ini); - } - else { + } else { initializeFromIg(params.rootDir, params.igPath, null); } - Encoding encoding = params.outputEncoding; - Boolean skipPackages = params.skipPackages; - Boolean includeELM = params.includeELM; - Boolean includeDependencies = params.includeDependencies; - Boolean includeTerminology = params.includeTerminology; - Boolean includePatientScenarios = params.includePatientScenarios; - Boolean addBundleTimestamp = params.addBundleTimestamp; - Boolean versioned = params.versioned; - String fhirUri = params.fhirUri; // String measureToRefreshPath = params.measureToRefreshPath; ArrayList resourceDirs = new ArrayList(); for (String resourceDir : params.resourceDirs) { @@ -107,20 +97,22 @@ public void publishIG(RefreshIGParameters params) { //Use case 3 //package everything LogUtils.info("IGProcessor.publishIG - bundleIg"); + Boolean skipPackages = params.skipPackages; + if (!skipPackages) { - igBundleProcessor.bundleIg( + new IGBundleProcessor(params.includeErrors, new LibraryProcessor(), new CDSHooksProcessor()).bundleIg( refreshedResourcesNames, rootDir, getBinaryPaths(), - encoding, - includeELM, - includeDependencies, - includeTerminology, - includePatientScenarios, - versioned, - addBundleTimestamp, + params.outputEncoding, + params.includeELM, + params.includeDependencies, + params.includeTerminology, + params.includePatientScenarios, + params.versioned, + params.addBundleTimestamp, fhirContext, - fhirUri + params.fhirUri ); } //test everything @@ -129,55 +121,49 @@ public void publishIG(RefreshIGParameters params) { } public ArrayList refreshedResourcesNames = new ArrayList(); + public void refreshIG(RefreshIGParameters params) { if (params.ini != null) { initializeFromIni(params.ini); - } - else { + } else { try { initializeFromIg(params.rootDir, params.igPath, null); - } - catch (Exception e) { + } catch (Exception e) { logMessage(String.format("Error Refreshing for File %s: %s", params.igPath, e.getMessage())); } } - Encoding encoding = params.outputEncoding; - // Boolean includeELM = params.includeELM; - // Boolean includeDependencies = params.includeDependencies; - String libraryPath = params.libraryPath; - String libraryOutputPath = params.libraryOutputPath; - String measureOutputPath = params.measureOutputPath; - Boolean includeTerminology = params.includeTerminology; - Boolean includePatientScenarios = params.includePatientScenarios; - Boolean versioned = params.versioned; - // String fhirUri = params.fhirUri; - String measureToRefreshPath = params.measureToRefreshPath; List resourceDirs = params.resourceDirs; - if (resourceDirs.size() == 0) { + if (resourceDirs.isEmpty()) { try { resourceDirs = IGUtils.extractResourcePaths(this.rootDir, this.sourceIg); } catch (IOException e) { - e.printStackTrace(); + logMessage(String.format("Error Extracting Resource Paths for File %s: %s", params.igPath, e.getMessage())); } } - IOUtils.resourceDirectories.addAll(resourceDirs); - FhirContext fhirContext = IGProcessor.getIgFhirContext(fhirVersion); + String measureToRefreshPath = params.measureToRefreshPath; + Encoding encoding = params.outputEncoding; + String measureOutputPath = params.measureOutputPath; + Boolean includePatientScenarios = params.includePatientScenarios; + Boolean versioned = params.versioned; - IGProcessor.ensure(rootDir, includePatientScenarios, includeTerminology, IOUtils.resourceDirectories); - List refreshedLibraryNames; - refreshedLibraryNames = libraryProcessor.refreshIgLibraryContent(this, encoding, libraryPath, libraryOutputPath, versioned, fhirContext, params.shouldApplySoftwareSystemStamp); - refreshedResourcesNames.addAll(refreshedLibraryNames); + IOUtils.resourceDirectories.addAll(resourceDirs); + FhirContext fhirContext = IGProcessor.getIgFhirContext(fhirVersion); + IGProcessor.ensure(rootDir, includePatientScenarios, params.includeTerminology, IOUtils.resourceDirectories); + + refreshedResourcesNames.addAll(new LibraryProcessor() + .refreshIgLibraryContent(this, encoding, params.libraryPath, params.libraryOutputPath, + versioned, fhirContext, params.shouldApplySoftwareSystemStamp)); - List refreshedMeasureNames; if (Strings.isNullOrEmpty(measureOutputPath)) { - refreshedMeasureNames = measureProcessor.refreshIgMeasureContent(this, encoding, versioned, fhirContext, measureToRefreshPath, params.shouldApplySoftwareSystemStamp); + refreshedResourcesNames.addAll(new MeasureProcessor().refreshIgMeasureContent(this, encoding, versioned, + fhirContext, measureToRefreshPath, params.shouldApplySoftwareSystemStamp)); } else { - refreshedMeasureNames = measureProcessor.refreshIgMeasureContent(this, encoding, measureOutputPath, versioned, fhirContext, measureToRefreshPath, params.shouldApplySoftwareSystemStamp); + refreshedResourcesNames.addAll(new MeasureProcessor().refreshIgMeasureContent(this, encoding, measureOutputPath, + versioned, fhirContext, measureToRefreshPath, params.shouldApplySoftwareSystemStamp)); } - refreshedResourcesNames.addAll(refreshedMeasureNames); if (refreshedResourcesNames.isEmpty()) { LogUtils.info("No resources successfully refreshed."); @@ -186,12 +172,11 @@ public void refreshIG(RefreshIGParameters params) { if (includePatientScenarios) { TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); - testCaseProcessor.refreshTestCases(FilenameUtils.concat(rootDir, IGProcessor.testCasePathElement), encoding, fhirContext, refreshedResourcesNames); + testCaseProcessor.refreshTestCases(FilenameUtils.concat(rootDir, IGProcessor.TEST_CASE_PATH_ELEMENT), encoding, fhirContext, refreshedResourcesNames, includeErrors); } } - public static FhirContext getIgFhirContext(String igVersion) - { + public static FhirContext getIgFhirContext(String igVersion) { if (igVersion == null) { throw new IllegalArgumentException(IG_VERSION_REQUIRED); } @@ -208,43 +193,36 @@ public static FhirContext getIgFhirContext(String igVersion) default: throw new IllegalArgumentException("Unknown IG version: " + igVersion); - } + } } - + public static final String bundlePathElement = "bundles/"; + public static String getBundlesPath(String igPath) { return FilenameUtils.concat(igPath, bundlePathElement); } - public static final String cqlLibraryPathElement = "input/pagecontent/cql/"; - public static final String libraryPathElement = "input/resources/library/"; - public static final String measurePathElement = "input/resources/measure/"; - public static final String planDefinitionPathElement = "input/resources/plandefinition/"; - public static final String valuesetsPathElement = "input/vocabulary/valueset/"; - 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, List resourcePaths) { File directory = new File(getBundlesPath(igPath)); if (!directory.exists()) { directory.mkdir(); - } - if (resourcePaths.isEmpty()) { - ensureDirectory(igPath, IGProcessor.cqlLibraryPathElement); - ensureDirectory(igPath, IGProcessor.libraryPathElement); - ensureDirectory(igPath, IGProcessor.measurePathElement); - ensureDirectory(igPath, IGProcessor.planDefinitionPathElement); - ensureDirectory(igPath, IGProcessor.valuesetsPathElement); - ensureDirectory(igPath, IGProcessor.testCasePathElement); } - else { - checkForDirectory(igPath, IGProcessor.cqlLibraryPathElement); - checkForDirectory(igPath, IGProcessor.libraryPathElement); - checkForDirectory(igPath, IGProcessor.measurePathElement); - checkForDirectory(igPath, IGProcessor.planDefinitionPathElement); - checkForDirectory(igPath, IGProcessor.valuesetsPathElement); - checkForDirectory(igPath, IGProcessor.testCasePathElement); + if (resourcePaths.isEmpty()) { + ensureDirectory(igPath, IGProcessor.CQL_LIBRARY_PATH_ELEMENT); + ensureDirectory(igPath, IGProcessor.LIBRARY_PATH_ELEMENT); + ensureDirectory(igPath, IGProcessor.MEASURE_PATH_ELEMENT); + ensureDirectory(igPath, IGProcessor.PLAN_DEFINITION_PATH_ELEMENT); + ensureDirectory(igPath, IGProcessor.VALUE_SETS_PATH_ELEMENT); + ensureDirectory(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT); + } else { + checkForDirectory(igPath, IGProcessor.CQL_LIBRARY_PATH_ELEMENT); + checkForDirectory(igPath, IGProcessor.LIBRARY_PATH_ELEMENT); + checkForDirectory(igPath, IGProcessor.MEASURE_PATH_ELEMENT); + checkForDirectory(igPath, IGProcessor.PLAN_DEFINITION_PATH_ELEMENT); + checkForDirectory(igPath, IGProcessor.VALUE_SETS_PATH_ELEMENT); + checkForDirectory(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT); } - checkForDirectory(igPath, IGProcessor.devicePathElement); + checkForDirectory(igPath, IGProcessor.DEVICE_PATH_ELEMENT); // HashSet cqlContentPaths = IOUtils.getCqlLibraryPaths(); } @@ -264,8 +242,7 @@ private static void checkForDirectory(String igPath, String pathElement) { File directory = new File(FilenameUtils.concat(igPath, pathElement)); if (!directory.exists()) { logger.info("No directory found by convention for: {}", directory.getName()); - } - else { + } else { // TODO: This is a concept different from "resource directories". It is expected elsewhere (e.g., IOUtils.setupActivityDefinitionPaths) // that resourceDirectories contains a set of proper "resource" directories. Adding non-resource directories // leads to surprising results when bundling like picking up resources from the /tests directory. 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 5c5ca7cd6..dba5aeed6 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 @@ -119,12 +119,12 @@ private CqfmSoftwareSystem getCqfRulerSoftwareSystem(String testServerUri) { org.hl7.fhir.dstu3.model.CapabilityStatement capabilityStatement = (org.hl7.fhir.dstu3.model.CapabilityStatement)resource; org.hl7.fhir.dstu3.model.Extension softwareModuleExtension = - capabilityStatement.getSoftware() - .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/capabilitystatement-softwareModule") - .stream() - .filter(extension -> !extension.getExtensionString("name").equals(BaseCqfmSoftwareSystemHelper.cqfRulerDeviceName)) - .collect(Collectors.toList()) - .get(0); + capabilityStatement.getSoftware() + .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/capabilitystatement-softwareModule") + .stream() + .filter(extension -> !extension.getExtensionString("name").equals(BaseCqfmSoftwareSystemHelper.cqfRulerDeviceName)) + .collect(Collectors.toList()) + .get(0); if (softwareModuleExtension != null) { softwareVersion = softwareModuleExtension.getExtensionString("version");//.getValue().toString(); @@ -133,12 +133,12 @@ private CqfmSoftwareSystem getCqfRulerSoftwareSystem(String testServerUri) { org.hl7.fhir.r4.model.CapabilityStatement capabilityStatement = (org.hl7.fhir.r4.model.CapabilityStatement)resource; org.hl7.fhir.r4.model.Extension softwareModuleExtension = - capabilityStatement.getSoftware() - .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/capabilitystatement-softwareModule") - .stream() - .filter(extension -> !extension.getExtensionString("name").equals(BaseCqfmSoftwareSystemHelper.cqfRulerDeviceName)) - .collect(Collectors.toList()) - .get(0); + capabilityStatement.getSoftware() + .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/capabilitystatement-softwareModule") + .stream() + .filter(extension -> !extension.getExtensionString("name").equals(BaseCqfmSoftwareSystemHelper.cqfRulerDeviceName)) + .collect(Collectors.toList()) + .get(0); if (softwareModuleExtension != null) { softwareVersion = softwareModuleExtension.getExtensionString("version");//.getValue().toString(); @@ -172,7 +172,7 @@ public void testIg(TestIGParameters params) { CqfmSoftwareSystem testTargetSoftwareSystem = getCqfRulerSoftwareSystem(params.fhirServerUri); - logger.info("Running IG test cases..."); + System.out.println("\r\n[Running IG Test Cases]\r\n"); File testCasesDirectory = new File(params.testCasesPath); if (!testCasesDirectory.isDirectory()) { @@ -180,97 +180,101 @@ public void testIg(TestIGParameters params) { } // refresh/generate test bundles - logger.info("Refreshing test cases..."); + System.out.println("\r\n[Refreshing Test Cases]\r\n"); + TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); - testCaseProcessor.refreshTestCases(params.testCasesPath, IOUtils.Encoding.JSON, fhirContext); + testCaseProcessor.refreshTestCases(params.testCasesPath, IOUtils.Encoding.JSON, fhirContext, includeErrors); List TestResults = new ArrayList(); - File[] resourceTypeTestGroups = testCasesDirectory.listFiles(file -> file.isDirectory()); + File[] resourceTypeTestGroups = testCasesDirectory.listFiles(File::isDirectory); //TODO: How can we validate the set of directories here - that they're actually FHIR resources - and message when they're not. Really it doesn't matter, it can be any grouping so long as it has a corresponding path in /bundles. - for (File group : resourceTypeTestGroups) { - logger.info("Processing {} test cases...", group.getName()); + if (resourceTypeTestGroups != null) { + for (File group : resourceTypeTestGroups) { + logger.info("Processing {} test cases...", group.getName()); - // Get set of test artifacts - File[] testArtifactNames = group.listFiles(file -> file.isDirectory()); + // Get set of test artifacts + File[] testArtifactNames = group.listFiles(File::isDirectory); - for (File testArtifact : testArtifactNames) { - logger.info("Processing test cases for {}: {}", group.getName(), testArtifact.getName()); - Boolean allTestArtifactTestsPassed = true; + if (testArtifactNames != null) { + for (File testArtifact : testArtifactNames) { + logger.info("Processing test cases for {}: {}", group.getName(), testArtifact.getName()); - // Get content bundle - Map.Entry testArtifactContentBundleMap = getContentBundleForTestArtifact(group.getName(), testArtifact.getName()); + // Get content bundle + Map.Entry testArtifactContentBundleMap = getContentBundleForTestArtifact(group.getName(), testArtifact.getName()); - if ((testArtifactContentBundleMap == null) || testArtifactContentBundleMap.getValue() == null) { - logger.info("No content bundle found for {}: {}", group.getName(), testArtifact.getName()); - logger.info("Done processing all test cases for {}: {}", group.getName(), testArtifact.getName()); - continue; - } + if ((testArtifactContentBundleMap == null) || testArtifactContentBundleMap.getValue() == null) { + logger.info("No content bundle found for {}: {}", group.getName(), testArtifact.getName()); + logger.info("Done processing all test cases for {}: {}", group.getName(), testArtifact.getName()); + continue; + } - ITestProcessor testProcessor = getResourceTypeTestProcessor(group.getName()); - List> testCasesBundles = - BundleUtils.getBundlesInDir(testArtifact.getPath(), fhirContext, false); - - for (Map.Entry testCaseBundleMapEntry : testCasesBundles) { - IBaseResource testCaseBundle = testCaseBundleMapEntry.getValue(); - TestCaseResultSummary testCaseResult = new TestCaseResultSummary(group.getName(), testArtifact.getName(), - testCaseBundle.getIdElement().toString()); - try { - logger.info("Starting processing of test case '{}' for {}: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName()); - Parameters testResults = testProcessor.executeTest(testCaseBundle, testArtifactContentBundleMap.getValue(), params.fhirServerUri); - - Boolean testPassed = false; - for (ParametersParameter param : testResults.getParameter()) { - if (param.getName().getValue().indexOf(MeasureTestProcessor.TestPassedKey) >= 0) { - testPassed = param.getValueBoolean().isValue(); - break; + ITestProcessor testProcessor = getResourceTypeTestProcessor(group.getName()); + List> testCasesBundles = + BundleUtils.getBundlesInDir(testArtifact.getPath(), fhirContext, false); + + for (Map.Entry testCaseBundleMapEntry : testCasesBundles) { + IBaseResource testCaseBundle = testCaseBundleMapEntry.getValue(); + TestCaseResultSummary testCaseResult = new TestCaseResultSummary(group.getName(), testArtifact.getName(), + testCaseBundle.getIdElement().toString()); + try { + logger.info("Starting processing of test case '{}' for {}: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName()); + Parameters testResults = testProcessor.executeTest(testCaseBundle, testArtifactContentBundleMap.getValue(), params.fhirServerUri); + + Boolean testPassed = false; + for (ParametersParameter param : testResults.getParameter()) { + if (param.getName().getValue().contains(MeasureTestProcessor.TestPassedKey)) { + testPassed = param.getValueBoolean().isValue(); + break; + } + } + testCaseResult.setTestPassed(testPassed); + logger.info("Done processing test case '{}' for {}: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName()); + } catch (Exception ex) { + testCaseResult.setTestPassed(false); + testCaseResult.setMessage(ex.getMessage()); + logger.error("Error: Test case '{}' for {}: {} failed with message: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName(), ex.getMessage()); } + TestResults.add(testCaseResult); } - testCaseResult.setTestPassed(testPassed); - logger.info("Done processing test case '{}' for {}: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName()); - } catch (Exception ex) { - testCaseResult.setTestPassed(false); - testCaseResult.setMessage(ex.getMessage()); - logger.error("Error: Test case '{}' for {}: {} failed with message: {}", testCaseBundle.getIdElement(), group.getName(), testArtifact.getName(), ex.getMessage()); - } - TestResults.add(testCaseResult); - } - logger.info(String.format(" Done processing all test cases for %s: %s", group.getName(), testArtifact.getName())); + logger.info(String.format(" Done processing all test cases for %s: %s", group.getName(), testArtifact.getName())); - if (allTestArtifactTestsPassed) { - List softwareSystems = new ArrayList() { - { - add(testTargetSoftwareSystem); - } - }; - - if ((fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) || (fhirContext.getVersion().getVersion() == FhirVersionEnum.R4)) { - if (fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { - // Stamp the testContentBundle artifacts - BundleUtils.stampDstu3BundleEntriesWithSoftwareSystems((org.hl7.fhir.dstu3.model.Bundle)testArtifactContentBundleMap.getValue(), softwareSystems, fhirContext, getRootDir()); - } else if (fhirContext.getVersion().getVersion() == FhirVersionEnum.R4) { - BundleUtils.stampR4BundleEntriesWithSoftwareSystems((org.hl7.fhir.r4.model.Bundle)testArtifactContentBundleMap.getValue(), softwareSystems, fhirContext, getRootDir()); - } + //all Test Artifact Tests Passed + List softwareSystems = new ArrayList() { + { + add(testTargetSoftwareSystem); + } + }; + + if ((fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) || (fhirContext.getVersion().getVersion() == FhirVersionEnum.R4)) { + if (fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { + // Stamp the testContentBundle artifacts + BundleUtils.stampDstu3BundleEntriesWithSoftwareSystems((org.hl7.fhir.dstu3.model.Bundle)testArtifactContentBundleMap.getValue(), softwareSystems, fhirContext, getRootDir()); + } else if (fhirContext.getVersion().getVersion() == FhirVersionEnum.R4) { + BundleUtils.stampR4BundleEntriesWithSoftwareSystems((org.hl7.fhir.r4.model.Bundle)testArtifactContentBundleMap.getValue(), softwareSystems, fhirContext, getRootDir()); + } - String bundleFilePath = testArtifactContentBundleMap.getKey(); - IBaseResource bundle = testArtifactContentBundleMap.getValue(); - IOUtils.writeResource(bundle, bundleFilePath, IOUtils.getEncoding(bundleFilePath), fhirContext); + String bundleFilePath = testArtifactContentBundleMap.getKey(); + IBaseResource bundle = testArtifactContentBundleMap.getValue(); + IOUtils.writeResource(bundle, bundleFilePath, IOUtils.getEncoding(bundleFilePath), fhirContext); + } } } - } - logger.info("Done processing {} test cases", group.getName()); + logger.info("Done processing {} test cases", group.getName()); + } } TestCaseResultSummaryComparator comparator = new TestCaseResultSummaryComparator(); - Collections.sort(TestResults, comparator); + TestResults.sort(comparator); List passedTests = new ArrayList(); List failedTests = new ArrayList(); for (TestCaseResultSummary result : TestResults) { - logger.info(result.toString()); + logger.info("TestCaseResultSummary: " + result.toString()); + if (result.testPassed) { passedTests.add(result); } else { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java index 8404adc8e..57b0c782f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java @@ -24,4 +24,6 @@ public interface IProcessorContext { List getBinaryPaths(); CqlProcessor getCqlProcessor(); + + Boolean getIncludeErrors(); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java similarity index 72% rename from tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessor.java rename to tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java index 9298316fc..abd9334eb 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java @@ -1,5 +1,7 @@ package org.opencds.cqf.tooling.processor; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; @@ -9,10 +11,10 @@ import ca.uhn.fhir.context.FhirContext; -public class PlanDefinitionProcessor extends AbstractResourceProcessor { +public class PlanDefinitionBundler extends AbstractBundler { @SuppressWarnings("this-escape") - public PlanDefinitionProcessor(LibraryProcessor libraryProcessor, CDSHooksProcessor cdsHooksProcessor) { + public PlanDefinitionBundler(LibraryProcessor libraryProcessor, CDSHooksProcessor cdsHooksProcessor) { setLibraryProcessor(libraryProcessor); setCDSHooksProcessor(cdsHooksProcessor); } @@ -34,12 +36,14 @@ protected String getResourceProcessorType() { } @Override - protected Set getPaths(FhirContext fhirContext) { - return IOUtils.getPlanDefinitionPaths(fhirContext); + protected List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { + //do nothing + return new ArrayList<>(); } @Override - protected void persistTestFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { - //not needed + protected Set getPaths(FhirContext fhirContext) { + return IOUtils.getPlanDefinitionPaths(fhirContext); } + } \ No newline at end of file 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 634c4b44a..61a2ea366 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 @@ -32,27 +32,27 @@ private FHIRVersion(String string) { public static FHIRVersion parse(String value) { switch (value) { - case "fhir3": - return FHIR3; - case "fhir4": - return FHIR4; - default: - throw new RuntimeException("Unable to parse FHIR version value:" + value); + case "fhir3": + return FHIR3; + case "fhir4": + return FHIR4; + default: + throw new RuntimeException("Unable to parse FHIR version value:" + value); } } } public static FhirContext getFhirContext(FHIRVersion fhirVersion) - { - switch (fhirVersion) { - case FHIR3: - return FhirContext.forDstu3Cached(); - case FHIR4: - return FhirContext.forR4Cached(); - default: - throw new IllegalArgumentException("Unknown IG version: " + fhirVersion); - } + { + switch (fhirVersion) { + case FHIR3: + return FhirContext.forDstu3Cached(); + case FHIR4: + return FhirContext.forR4Cached(); + default: + throw new IllegalArgumentException("Unknown IG version: " + fhirVersion); } + } public static void PostBundlesInDir(PostBundlesInDirParameters params) { String fhirUri = params.fhirUri; @@ -62,16 +62,20 @@ public static void PostBundlesInDir(PostBundlesInDirParameters params) { List> resources = BundleUtils.getBundlesInDir(params.directoryPath, fhirContext); resources.forEach(entry -> postBundleToFhirUri(fhirUri, encoding, fhirContext, entry.getValue())); + + if (HttpClientUtils.hasPostTasksInQueue()){ + HttpClientUtils.postTaskCollection(); + } } - private static void postBundleToFhirUri(String fhirUri, Encoding encoding, FhirContext fhirContext, IBaseResource bundle) { - if (fhirUri != null && !fhirUri.equals("")) { + private static void postBundleToFhirUri(String fhirUri, Encoding encoding, FhirContext fhirContext, IBaseResource bundle) { + if (fhirUri != null && !fhirUri.equals("")) { try { - HttpClientUtils.post(fhirUri, bundle, encoding, fhirContext); + HttpClientUtils.post(fhirUri, bundle, encoding, fhirContext, null); logger.info("Resource successfully posted to FHIR server ({}): {}", fhirUri, bundle.getIdElement().getIdPart()); } catch (Exception e) { logger.error("Error occurred for element {}: {}",bundle.getIdElement().getIdPart(), e.getMessage()); - } + } } } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index fafda6108..c0a5663a2 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -1,119 +1,216 @@ package org.opencds.cqf.tooling.processor; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.IFhirVersion; +import jakarta.annotation.Nullable; import org.apache.commons.io.FilenameUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Reference; +import org.opencds.cqf.tooling.common.ThreadUtils; import org.opencds.cqf.tooling.utilities.BundleUtils; import org.opencds.cqf.tooling.utilities.IOUtils; -import org.opencds.cqf.tooling.utilities.LogUtils; import org.opencds.cqf.tooling.utilities.ResourceUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.model.api.IFhirVersion; +public class TestCaseProcessor { + public static final String NEWLINE_INDENT = "\r\n\t"; + public static final String NEWLINE = "\r\n"; -public class TestCaseProcessor -{ + public static final String separator = System.getProperty("file.separator"); private static final Logger logger = LoggerFactory.getLogger(TestCaseProcessor.class); - public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext) { - refreshTestCases(path, encoding, fhirContext, null); + public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, Boolean includeErrors) { + refreshTestCases(path, encoding, fhirContext, null, includeErrors); } - public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, @Nullable List refreshedResourcesNames) - { - logger.info("Refreshing tests"); - List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); - IFhirVersion version = fhirContext.getVersion(); + public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, @Nullable List refreshedResourcesNames, + Boolean includeErrors) { + System.out.println("\r\n[Refreshing Tests]\r\n"); + + + final Map testCaseRefreshSuccessMap = new ConcurrentHashMap<>(); + final Map testCaseRefreshFailMap = new ConcurrentHashMap<>(); + final Map groupFileRefreshSuccessMap = new ConcurrentHashMap<>(); + final Map groupFileRefreshFailMap = new ConcurrentHashMap<>(); + final List> testCaseRefreshTasks = new CopyOnWriteArrayList<>(); + IFhirVersion version = fhirContext.getVersion(); + //build list of tasks via for loop: + List> testGroupTasks = new ArrayList<>(); + ExecutorService testGroupExecutor = Executors.newCachedThreadPool(); + List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); for (String group : resourceTypeTestGroups) { - List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); - for (String testArtifactPath : testArtifactPaths) { - List testCasePaths = IOUtils.getDirectoryPaths(testArtifactPath, false); + testGroupTasks.add(() -> { + List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); + + //build list of tasks via for loop: + List> testArtifactTasks = new CopyOnWriteArrayList<>(); + ExecutorService testArtifactExecutor = Executors.newCachedThreadPool(); + + for (String testArtifactPath : testArtifactPaths) { + testArtifactTasks.add(() -> { + List testCasePaths = IOUtils.getDirectoryPaths(testArtifactPath, false); + + org.hl7.fhir.r4.model.Group testGroup; + if (version.getVersion() == FhirVersionEnum.R4) { + testGroup = new org.hl7.fhir.r4.model.Group(); + testGroup.setActive(true); + testGroup.setType(Group.GroupType.PERSON); + testGroup.setActual(true); + } else { + testGroup = null; + } - org.hl7.fhir.r4.model.Group testGroup = null; + // For each test case we need to create a group + if (!testCasePaths.isEmpty()) { + String measureName = IOUtils.getMeasureTestDirectory(testCasePaths.get(0)); - if (version.getVersion() == FhirVersionEnum.R4) { - testGroup = new org.hl7.fhir.r4.model.Group(); - testGroup.setActive(true); - testGroup.setType(Group.GroupType.PERSON); - testGroup.setActual(true); - } + if (testGroup != null) { + testGroup.setId(measureName); - // For each test case we need to create a group - if (!testCasePaths.isEmpty()) { - String measureName = IOUtils.getMeasureTestDirectory(testCasePaths.get(0)); - if (testGroup != null) { - testGroup.setId(measureName); - - testGroup.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-testArtifact", - new CanonicalType("http://ecqi.healthit.gov/ecqms/Measure/" + measureName)); - } - - for (String testCasePath : testCasePaths) { - try { - List paths = IOUtils.getFilePaths(testCasePath, true); - List resources = IOUtils.readResources(paths, fhirContext); - ensureIds(testCasePath, resources); - - // Loop through resources and any that are patients need to be added to the test Group - // Handle individual resources when they exist - for (IBaseResource resource : resources) { - if ((resource.fhirType() == "Patient") && (version.getVersion() == FhirVersionEnum.R4)) { - org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) resource; - addPatientToGroupR4(testGroup, patient); - } + testGroup.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-testArtifact", + new CanonicalType("http://ecqi.healthit.gov/ecqms/Measure/" + measureName)); + } + + for (String testCasePath : testCasePaths) { + testCaseRefreshTasks.add(() -> { + try { + List paths = IOUtils.getFilePaths(testCasePath, true); + List resources = IOUtils.readResources(paths, fhirContext); + ensureIds(testCasePath, resources); + + // Loop through resources and any that are patients need to be added to the test Group + // Handle individual resources when they exist + for (IBaseResource resource : resources) { + if ((resource.fhirType().equalsIgnoreCase("Patient")) && (version.getVersion() == FhirVersionEnum.R4)) { + org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) resource; + if (testGroup != null) { + addPatientToGroupR4(testGroup, patient); + } + } + + // Handle bundled resources when that is how they are provided + if ((resource.fhirType().equalsIgnoreCase("Bundle")) && (version.getVersion() == FhirVersionEnum.R4)) { + org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) resource; + var bundleResources = + BundleUtils.getR4ResourcesFromBundle(bundle); + for (IBaseResource bundleResource : bundleResources) { + if (bundleResource.fhirType().equalsIgnoreCase("Patient")) { + org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) bundleResource; + if (testGroup != null) { + addPatientToGroupR4(testGroup, patient); + } + } + } + } + } - // Handle bundled resources when that is how they are provided - if ((resource.fhirType() == "Bundle") && (version.getVersion() == FhirVersionEnum.R4)) { - org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) resource; - var bundleResources = - BundleUtils.getR4ResourcesFromBundle(bundle); - for (IBaseResource bundleResource : bundleResources) { - if (bundleResource.fhirType() == "Patient") { - org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) bundleResource; - addPatientToGroupR4(testGroup, patient); + // If the resource is a transaction bundle then don't bundle it again otherwise do + String fileId = getId(FilenameUtils.getName(testCasePath)); + Object bundle; + if ((resources.size() == 1) && (BundleUtils.resourceIsABundle(resources.get(0)))) { + bundle = processTestBundle(fileId, resources.get(0), fhirContext, testArtifactPath, testCasePath); + } else { + bundle = BundleUtils.bundleArtifacts(fileId, resources, fhirContext, false); } + IOUtils.writeBundle(bundle, testArtifactPath, encoding, fhirContext); + + } catch (Exception e) { + testCaseRefreshFailMap.put(testCasePath, e.getMessage()); } + + + testCaseRefreshSuccessMap.put(testCasePath, ""); + reportProgress((testCaseRefreshFailMap.size() + testCaseRefreshSuccessMap.size()), testCaseRefreshTasks.size()); + //task requires return statement + return null; + }); + }//end for (String testCasePath : testCasePaths) { + + // Need to output the Group if it exists + if (testGroup != null) { + String groupFileName = "Group-" + measureName; + String groupFileIdentifier = testArtifactPath + separator + groupFileName; + + try { + IOUtils.writeResource(testGroup, testArtifactPath, encoding, fhirContext, true, + groupFileName); + + groupFileRefreshSuccessMap.put(groupFileIdentifier, ""); + + } catch (Exception e) { + + groupFileRefreshFailMap.put(groupFileIdentifier, e.getMessage()); } - } - // If the resource is a transaction bundle then don't bundle it again otherwise do - String fileId = getId(FilenameUtils.getName(testCasePath)); - Object bundle; - if ((resources.size() == 1) && (BundleUtils.resourceIsABundle(resources.get(0)))) { - bundle = processTestBundle(fileId, resources.get(0), fhirContext); - }else { - bundle = BundleUtils.bundleArtifacts(fileId, resources, fhirContext, false); } - IOUtils.writeBundle(bundle, testArtifactPath, encoding, fhirContext); - - } catch (Exception e) { - LogUtils.putException(testCasePath, e); - } finally { - LogUtils.warn(testCasePath); } - } + //task requires return statement + return null; + }); + }// + ThreadUtils.executeTasks(testArtifactTasks, testArtifactExecutor); + + //task requires return statement + return null; + }); + }//end for (String group : resourceTypeTestGroups) { + ThreadUtils.executeTasks(testGroupTasks, testGroupExecutor); + //Now with all possible tasks collected, progress can be reported instead of flooding the console. + ThreadUtils.executeTasks(testCaseRefreshTasks); + //ensure accurate progress at final stage: + reportProgress((testCaseRefreshFailMap.size() + testCaseRefreshSuccessMap.size()), testCaseRefreshTasks.size()); + + StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", includeErrors); + if (!groupFileRefreshSuccessMap.isEmpty() || !groupFileRefreshFailMap.isEmpty()) { + testCaseMessage.append(buildInformationMessage(groupFileRefreshFailMap, groupFileRefreshSuccessMap, "Group File", "Created", includeErrors)); + } + System.out.println(testCaseMessage); + } - // Need to output the Group if it exists - if (testGroup != null) { - IOUtils.writeResource(testGroup, testArtifactPath, encoding, fhirContext, true, - "Group-" + measureName); - } + /** + * Gives the user a nice report at the end of the refresh test case process (used to report group file status as well) + * + * @param failMap which items failed + * @param successMap which items succeeded + * @param type group file or test case + * @param successType created or refreshed + * @param includeErrors give the exception message if includeErrors is on + * @return built message for console + */ + private StringBuilder buildInformationMessage(Map failMap, Map successMap, String type, String successType, boolean includeErrors) { + StringBuilder message = new StringBuilder(); + if (!successMap.isEmpty() || !failMap.isEmpty()) { + message.append(NEWLINE).append(successMap.size()).append(" ").append(type).append("(s) successfully ").append(successType.toLowerCase()).append(":"); + for (String refreshedTestCase : successMap.keySet()) { + message.append(NEWLINE_INDENT).append(refreshedTestCase).append(" ").append(successType.toUpperCase()); + } + if (!failMap.isEmpty()) { + message.append(NEWLINE).append(failMap.size()).append(" ").append(type).append("(s) failed to be ").append(successType.toLowerCase()).append(":"); + for (String failed : failMap.keySet()) { + message.append(NEWLINE_INDENT).append(failed).append(" FAILED").append(includeErrors ? ": " + failMap.get(failed) : ""); } } } + return message; } - public static Object processTestBundle(String id, IBaseResource resource, FhirContext fhirContext) { + private void reportProgress(int count, int total) { + double percentage = (double) count / total * 100; + System.out.print("\rTest Refresh: " + String.format("%.2f%%", percentage) + " processed."); + } + + public static Object processTestBundle(String id, IBaseResource resource, FhirContext fhirContext, String testArtifactPath, String testCasePath) { switch (fhirContext.getVersion().getVersion()) { case DSTU3: org.hl7.fhir.dstu3.model.Bundle dstu3Bundle = (org.hl7.fhir.dstu3.model.Bundle) resource; @@ -130,7 +227,7 @@ public static Object processTestBundle(String id, IBaseResource resource, FhirCo return dstu3Bundle; case R4: - org.hl7.fhir.r4.model.Bundle r4Bundle = (org.hl7.fhir.r4.model.Bundle)resource; + org.hl7.fhir.r4.model.Bundle r4Bundle = (org.hl7.fhir.r4.model.Bundle) resource; ResourceUtils.setIgId(id, r4Bundle, false); r4Bundle.setType(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION); for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent entry : r4Bundle.getEntry()) { @@ -187,7 +284,7 @@ public static String getId(String baseId) { public static Boolean bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, Map resources) { Boolean shouldPersist = true; - String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.testCasePathElement), contextResourceType), libraryName); + String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT), contextResourceType), libraryName); // this is breaking for bundle of a bundle. Replace with individual resources // until we can figure it out. @@ -213,68 +310,26 @@ public static Boolean bundleTestCases(String igPath, String contextResourceType, } - static Set copiedFilePaths = new HashSet<>(); - //TODO: the bundle needs to have -expectedresults added too + /** * Bundles test case files from the specified path into a destination path. * The method copies relevant test case files, including expected results for MeasureReports, * and returns a summary message with the number of files copied. * - * @param igPath The path to the Implementation Guide (IG) containing test case files. + * @param igPath The path to the Implementation Guide (IG) containing test case files. * @param contextResourceType The resource type associated with the test cases. - * @param libraryName The name of the library associated with the test cases. - * @param destPath The destination path for the bundled test case files. - * @param fhirContext The FHIR context used for reading and processing resources. - * @return A summary message indicating the number of files copied for the specified test case path. + * @param libraryName The name of the library associated with the test cases. + * @param destPath The destination path for the bundled test case files. + * @param fhirContext The FHIR context used for reading and processing resources. */ - public static String bundleTestCaseFiles(String igPath, String contextResourceType, String libraryName, String destPath, FhirContext fhirContext) { - String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.testCasePathElement), contextResourceType), libraryName); + public static void bundleTestCaseFiles(String igPath, String contextResourceType, String libraryName, String destPath, FhirContext fhirContext) { + String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT), contextResourceType), libraryName); List testCasePaths = IOUtils.getFilePaths(igTestCasePath, false); - Set measureReportPaths = IOUtils.getMeasureReportPaths(fhirContext); - List testCaseDirectories = IOUtils.getDirectoryPaths(igTestCasePath, false); - - int tracker = 0; for (String testPath : testCasePaths) { String bundleTestDestPath = FilenameUtils.concat(destPath, FilenameUtils.getName(testPath)); - if (IOUtils.copyFile(testPath, bundleTestDestPath)) { - tracker++; - } - - for (String testCaseDirectory : testCaseDirectories) { - List testContentPaths = IOUtils.getFilePaths(testCaseDirectory, false); - for (String testContentPath : testContentPaths) { - // Copy the file if it hasn't been copied before (Set.add returns false if the Set already contains this entry) - if (copiedFilePaths.add(testContentPath)) { - - Optional matchingMeasureReportPath = measureReportPaths.stream() - .filter(path -> path.equals(testContentPath)) - .findFirst(); - if (matchingMeasureReportPath.isPresent()) { - IBaseResource measureReport = IOUtils.readResource(testContentPath, fhirContext); - if (!measureReport.getIdElement().getIdPart().startsWith("measurereport") || !measureReport.getIdElement().getIdPart().endsWith("-expectedresults")) { - Object measureReportStatus = ResourceUtils.resolveProperty(measureReport, "status", fhirContext); - String measureReportStatusValue = ResourceUtils.resolveProperty(measureReportStatus, "value", fhirContext).toString(); - if (measureReportStatusValue.equals("COMPLETE")) { - String expectedResultsId = FilenameUtils.getBaseName(testContentPath) + (FilenameUtils.getBaseName(testContentPath).endsWith("-expectedresults") ? "" : "-expectedresults"); - measureReport.setId(expectedResultsId); - } - } - IOUtils.writeResource(measureReport, destPath, IOUtils.Encoding.JSON, fhirContext); - } else { - String bundleTestContentDestPath = FilenameUtils.concat(destPath, FilenameUtils.getName(testContentPath)); - if (IOUtils.copyFile(testContentPath, bundleTestContentDestPath)) { - tracker++; - } - } - } - } - } + IOUtils.copyFile(testPath, bundleTestDestPath); } - return "\nBundle Test Case Files: " + tracker + " files copied for " + igTestCasePath; } - public static void cleanUp() { - copiedFilePaths = new HashSet<>(); - } -} +} \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/ValueSetsProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/ValueSetsProcessor.java index ffaf13dcd..3e2e4fda3 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/ValueSetsProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/ValueSetsProcessor.java @@ -2,7 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.opencds.cqf.tooling.cql.exception.CQLTranslatorException; +import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; import org.opencds.cqf.tooling.utilities.ResourceUtils; @@ -77,7 +77,7 @@ public static String getId(String baseId) { } public static void bundleValueSets(String cqlContentPath, String igPath, FhirContext fhirContext, - Map resources, Encoding encoding, Boolean includeDependencies, Boolean includeVersion) throws CQLTranslatorException { + Map resources, Encoding encoding, Boolean includeDependencies, Boolean includeVersion) throws CqlTranslatorException { Map dependencies = ResourceUtils.getDepValueSetResources(cqlContentPath, igPath, fhirContext, includeDependencies, includeVersion); for (IBaseResource resource : dependencies.values()) { resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/argument/RefreshIGArgumentProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/argument/RefreshIGArgumentProcessor.java index 17d7769e6..e73be0cbd 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/argument/RefreshIGArgumentProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/argument/RefreshIGArgumentProcessor.java @@ -38,6 +38,8 @@ public class RefreshIGArgumentProcessor { public static final String[] MEASURE_OUTPUT_PATH_OPTIONS = {"measureOutput", "measureOutputPath", "mop"}; public static final String[] SHOULD_APPLY_SOFTWARE_SYSTEM_STAMP_OPTIONS = { "ss", "stamp" }; public static final String[] SHOULD_ADD_TIMESTAMP_OPTIONS = { "ts", "timestamp" }; + public static final String[] SHOULD_INCLUDE_ERRORS = { "x", "include-errors" }; + @SuppressWarnings("unused") public OptionParser build() { @@ -55,6 +57,7 @@ public OptionParser build() { OptionSpecBuilder measureOutputPathBuilder = parser.acceptsAll(asList(MEASURE_OUTPUT_PATH_OPTIONS),"If omitted, the measures will overwrite any existing measures"); OptionSpecBuilder shouldApplySoftwareSystemStampBuilder = parser.acceptsAll(asList(SHOULD_APPLY_SOFTWARE_SYSTEM_STAMP_OPTIONS),"Indicates whether refreshed Measure and Library resources should be stamped with the 'cqf-tooling' stamp via the cqfm-softwaresystem Extension."); OptionSpecBuilder shouldAddTimestampBuilder = parser.acceptsAll(asList(SHOULD_ADD_TIMESTAMP_OPTIONS),"Indicates whether refreshed Bundle should attach timestamp of creation."); + OptionSpecBuilder shouldIncludeErrors = parser.acceptsAll(asList(SHOULD_APPLY_SOFTWARE_SYSTEM_STAMP_OPTIONS),"Indicates that a complete list of errors during library, measure, and test case refresh are included upon failure."); OptionSpec ini = iniBuilder.withRequiredArg().describedAs("Path to the IG ini file"); OptionSpec rootDir = rootDirBuilder.withOptionalArg().describedAs("Root directory of the IG"); @@ -67,9 +70,11 @@ public OptionParser build() { OptionSpec measureOutputPath = measureOutputPathBuilder.withOptionalArg().describedAs("path to the output directory for updated measures"); OptionSpec shouldApplySoftwareSystemStamp = shouldApplySoftwareSystemStampBuilder.withOptionalArg().describedAs("Indicates whether refreshed Measure and Library resources should be stamped with the 'cqf-tooling' stamp via the cqfm-softwaresystem Extension"); OptionSpec shouldAddTimestampOptions = shouldAddTimestampBuilder.withOptionalArg().describedAs("Indicates whether refreshed Bundle should attach timestamp of creation"); + OptionSpec shouldIncludeErrorsOptions = shouldIncludeErrors.withOptionalArg().describedAs("Indicates that a complete list of errors during library, measure, and test case refresh are included upon failure."); + //TODO: FHIR user / password (and other auth options) - OptionSpec fhirUri = fhirUriBuilder.withOptionalArg().describedAs("uri of fhir server"); + OptionSpec fhirUri = fhirUriBuilder.withOptionalArg().describedAs("uri of fhir server"); parser.acceptsAll(asList(OPERATION_OPTIONS),"The operation to run."); parser.acceptsAll(asList(SKIP_PACKAGES_OPTIONS), "Specifies whether to skip packages building."); @@ -78,6 +83,7 @@ public OptionParser build() { parser.acceptsAll(asList(INCLUDE_TERMINOLOGY_OPTIONS),"If omitted terminology will not be packaged."); parser.acceptsAll(asList(INCLUDE_PATIENT_SCENARIOS_OPTIONS),"If omitted patient scenario information will not be packaged."); parser.acceptsAll(asList(VERSIONED_OPTIONS),"If omitted resources must be uniquely named."); + parser.acceptsAll(asList(SHOULD_INCLUDE_ERRORS),"Specifies whether to show errors during library, measure, and test case refresh."); OptionSpec help = parser.acceptsAll(asList(ArgUtils.HELP_OPTIONS), "Show this help page").forHelp(); @@ -103,7 +109,7 @@ public RefreshIGParameters parseAndConvert(String[] args) { if (libraryPaths != null && libraryPaths.size() == 1) { libraryPath = libraryPaths.get(0); } - + //could not easily use the built-in default here because it is based on the value of the igPath argument. String igEncoding = (String)options.valueOf(IG_OUTPUT_ENCODING[0]); Encoding outputEncodingEnum = Encoding.JSON; @@ -111,7 +117,7 @@ public RefreshIGParameters parseAndConvert(String[] args) { outputEncodingEnum = Encoding.parse(igEncoding.toLowerCase()); } Boolean skipPackages = options.has(SKIP_PACKAGES_OPTIONS[0]); - Boolean includeELM = options.has(INCLUDE_ELM_OPTIONS[0]); + Boolean includeELM = options.has(INCLUDE_ELM_OPTIONS[0]); Boolean includeDependencies = options.has(INCLUDE_DEPENDENCY_LIBRARY_OPTIONS[0]); Boolean includeTerminology = options.has(INCLUDE_TERMINOLOGY_OPTIONS[0]); Boolean includePatientScenarios = options.has(INCLUDE_PATIENT_SCENARIOS_OPTIONS[0]); @@ -143,6 +149,8 @@ public RefreshIGParameters parseAndConvert(String[] args) { addBundleTimestamp = true; } + Boolean includeErrors = options.has(SHOULD_INCLUDE_ERRORS[0]); + ArrayList paths = new ArrayList(); if (resourcePaths != null && !resourcePaths.isEmpty()) { paths.addAll(resourcePaths); @@ -150,7 +158,7 @@ public RefreshIGParameters parseAndConvert(String[] args) { if (libraryPaths != null) { paths.addAll(libraryPaths); } - + RefreshIGParameters ip = new RefreshIGParameters(); ip.ini = ini; ip.rootDir = rootDir; @@ -170,7 +178,8 @@ public RefreshIGParameters parseAndConvert(String[] args) { ip.measureToRefreshPath = measureToRefreshPath; ip.libraryOutputPath = libraryOutputPath; ip.measureOutputPath = measureOutputPath; - + ip.includeErrors = includeErrors; + return ip; } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java similarity index 70% rename from tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessor.java rename to tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java index b091f5fc6..1862f737a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java @@ -1,19 +1,21 @@ package org.opencds.cqf.tooling.questionnaire; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.processor.AbstractResourceProcessor; +import org.opencds.cqf.tooling.processor.AbstractBundler; import org.opencds.cqf.tooling.utilities.IOUtils; import ca.uhn.fhir.context.FhirContext; -public class QuestionnaireProcessor extends AbstractResourceProcessor { +public class QuestionnaireBundler extends AbstractBundler { @SuppressWarnings("this-escape") - public QuestionnaireProcessor(LibraryProcessor libraryProcessor) { + public QuestionnaireBundler(LibraryProcessor libraryProcessor) { setLibraryProcessor(libraryProcessor); } @@ -34,12 +36,14 @@ protected String getResourceProcessorType() { } @Override - protected Set getPaths(FhirContext fhirContext) { - return IOUtils.getQuestionnairePaths(fhirContext); + protected List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { + //do nothing + return new ArrayList<>(); } @Override - protected void persistTestFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { - //not needed + protected Set getPaths(FhirContext fhirContext) { + return IOUtils.getQuestionnairePaths(fhirContext); } + } \ No newline at end of file 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 8cc4a6319..45910539f 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 @@ -108,16 +108,6 @@ public static org.hl7.fhir.r4.model.Bundle bundleR4Artifacts(String id, List> getBundlesInDir(String directoryPath, FhirContext fhirContext) { return getBundlesInDir(directoryPath, fhirContext, true); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index 8dea7cfc7..7082e6d98 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -1,6 +1,7 @@ package org.opencds.cqf.tooling.utilities; import ca.uhn.fhir.context.FhirContext; +import com.google.gson.JsonParser; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -22,9 +23,9 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** @@ -36,16 +37,22 @@ public class HttpClientUtils { private static final String BUNDLE_RESOURCE = "Bundle Resource"; private static final String ENCODING_TYPE = "Encoding Type"; private static final String FHIR_CONTEXT = "FHIR Context"; + + //This is not to maintain a thread count, but rather to maintain the maximum number of POST calls that can simultaneously be waiting for a response from the server. + //This gives us some control over how many POSTs we're making so we don't crash the server. + //possible TODO:Allow users to specify this value on their own with arg passed into operation so that more robust servers can process post list faster private static final int MAX_SIMULTANEOUS_POST_COUNT = 10; //failedPostCalls needs to maintain the details built in the FAILED message, as well as a copy of the inputs for a retry by the user on failed posts. private static Queue> failedPostCalls = new ConcurrentLinkedQueue<>(); private static List successfulPostCalls = new CopyOnWriteArrayList<>(); - private static Map> tasks = new ConcurrentHashMap<>(); - private static List runningPostTaskList = new CopyOnWriteArrayList<>(); - private static final AtomicInteger counter = new AtomicInteger(0); + private static Map> tasks = new ConcurrentHashMap<>(); + private static Map> initialTasks = new ConcurrentHashMap<>(); + private static List runningPostTaskList = new CopyOnWriteArrayList<>(); + private static int processedPostCounter = 0; - private HttpClientUtils() {} + private HttpClientUtils() { + } public static boolean hasPostTasksInQueue() { return !tasks.isEmpty(); @@ -58,9 +65,10 @@ public static boolean hasPostTasksInQueue() { * @param resource The FHIR resource to be posted. * @param encoding The encoding type of the resource. * @param fhirContext The FHIR context for the resource. + * @param fileLocation Optional fileLocation indicator for identifying resources by raw filename * @throws IOException If an I/O error occurs during the request. */ - public static void post(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext) throws IOException { + public static void post(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext, String fileLocation, boolean withPriority) throws IOException { List missingValues = new ArrayList<>(); List values = new ArrayList<>(); validateAndAddValue(fhirServerUrl, FHIR_SERVER_URL, missingValues, values); @@ -72,11 +80,15 @@ public static void post(String fhirServerUrl, IBaseResource resource, IOUtils.En String missingValueString = String.join(", ", missingValues); System.out.println("An invalid HTTP POST call was attempted with a null value for: " + missingValueString + (!values.isEmpty() ? "\\nRemaining values are: " + String.join(", ", values) : "")); + return; } - createPostTask(fhirServerUrl, resource, encoding, fhirContext); + createPostTask(fhirServerUrl, resource, encoding, fhirContext, fileLocation, withPriority); } + public static void post(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext, String fileLocation) throws IOException { + post(fhirServerUrl, resource, encoding, fhirContext, fileLocation, false); + } /** * Validates a value and adds its representation to the provided lists using a custom value-to-string function. @@ -99,6 +111,7 @@ private static void validateAndAddValue(T value, String label, List values.add(label + ": " + valueToString.apply(value)); } } + private static void validateAndAddValue(T value, String label, List missingValues, List values) { validateAndAddValue(value, label, missingValues, values, Object::toString); } @@ -115,14 +128,15 @@ private static void validateAndAddValue(T value, String label, List * @param encoding The encoding type of the resource. * @param fhirContext The FHIR context for the resource. */ - private static void createPostTask(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext) { + private static void createPostTask(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext, String fileLocation, boolean withPriority) { try { - final int currentTaskIndex = tasks.size() + 1; - PostComponent postPojo = new PostComponent(fhirServerUrl, resource, encoding, fhirContext); + PostComponent postPojo = new PostComponent(fhirServerUrl, resource, encoding, fhirContext, fileLocation, withPriority); HttpPost post = configureHttpPost(fhirServerUrl, resource, encoding, fhirContext); - - Callable task = createPostCallable(post, postPojo, currentTaskIndex); - tasks.put(resource, task); + if (withPriority) { + initialTasks.put(resource.getIdElement().getIdPart(), createPostCallable(post, postPojo)); + } else { + tasks.put(resource.getIdElement().getIdPart(), createPostCallable(post, postPojo)); + } } catch (Exception e) { logger.error("Error while submitting the POST request: " + e.getMessage(), e); } @@ -142,10 +156,20 @@ private static void createPostTask(String fhirServerUrl, IBaseResource resource, * @return An HTTP POST request configured for the FHIR server and resource. */ private static HttpPost configureHttpPost(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext) { - HttpPost post = new HttpPost(fhirServerUrl); + + //Transaction bundles get posted to /fhir but other resources get posted to /fhir/resourceType ie fhir/Group + String fhirServer = fhirServerUrl; + if (!BundleUtils.resourceIsTransactionBundle(resource)) { + fhirServer = fhirServer + + (fhirServerUrl.endsWith("/") ? resource.fhirType() + : "/" + resource.fhirType()); + } + + HttpPost post = new HttpPost(fhirServer); post.addHeader("content-type", "application/" + encoding.toString()); String resourceString = IOUtils.encodeResourceAsString(resource, encoding, fhirContext); + StringEntity input; try { input = new StringEntity(resourceString); @@ -175,40 +199,67 @@ private static HttpPost configureHttpPost(String fhirServerUrl, IBaseResource re * 5. Updates the progress and status of the post task. * * @param post The HTTP POST request to be executed. - * @param postPojo A data object containing additional information about the POST request. - * @param currentTaskIndex The current index of the POST task among all tasks. + * @param postComponent A data object containing additional information about the POST request. * @return A callable task for executing the HTTP POST request. */ - private static Callable createPostCallable(HttpPost post, PostComponent postPojo, int currentTaskIndex) { + private static Callable createPostCallable(HttpPost post, PostComponent postComponent) { return () -> { + String resourceIdentifier = (postComponent.fileLocation != null ? + Paths.get(postComponent.fileLocation).getFileName().toString() + : + postComponent.resource.getIdElement().getIdPart()); try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { HttpResponse response = httpClient.execute(post); StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); - String reasonPhrase = statusLine.getReasonPhrase(); - String httpVersion = statusLine.getProtocolVersion().toString(); + String diagnosticString = getDiagnosticString(EntityUtils.toString(response.getEntity())); if (statusCode >= 200 && statusCode < 300) { - successfulPostCalls.add(currentTaskIndex + " out of " + tasks.size() + " - Resource successfully posted to FHIR server: " + postPojo.resource.getIdElement().getIdPart()); + successfulPostCalls.add("[SUCCESS] Resource successfully posted to " + postComponent.fhirServerUrl + ": " + resourceIdentifier); } else { - String detailedMessage = currentTaskIndex + " out of " + tasks.size() + " - Error posting resource to FHIR server (" + postPojo.fhirServerUrl - + ") " + postPojo.resource.getIdElement().getIdPart() + ": HTTP Status: " + statusCode + " " + reasonPhrase + " (HTTP Version: " + httpVersion + ")"; - - failedPostCalls.add(Pair.of(detailedMessage, postPojo)); + failedPostCalls.add(Pair.of("[FAIL] Error " + statusCode + " from " + postComponent.fhirServerUrl + ": " + resourceIdentifier + ": " + diagnosticString, postComponent)); } - } catch (IOException e) { - failedPostCalls.add(Pair.of(currentTaskIndex + " out of " + tasks.size() + " - Error while making the POST request: " + e.getMessage(), postPojo)); } catch (Exception e) { - failedPostCalls.add(Pair.of(currentTaskIndex + " out of " + tasks.size() + " - Error during POST request execution: " + e.getMessage(), postPojo)); + failedPostCalls.add(Pair.of("[FAIL] Exception during " + resourceIdentifier + " POST request execution to " + postComponent.fhirServerUrl + ": " + e.getMessage(), postComponent)); } - runningPostTaskList.remove(postPojo.resource); + runningPostTaskList.remove(postComponent.resource.getIdElement().getIdPart()); reportProgress(); return null; }; } + /** + * This method takes in a json string from the endpoint that might look like this: + * { + * "resourceType": "OperationOutcome", + * "issue": [ { + * "severity": "error", + * "code": "processing", + * "diagnostics": "HAPI-1094: Resource Condition/delivery-of-singleton-f83c not found, specified in path: Encounter.diagnosis.condition" + * } ] + * } + * It extracts the diagnostics and returns a string appendable to the response + * + * @param jsonString + * @return + */ + private static String getDiagnosticString(String jsonString) { + try { + // Get the "diagnostics" property + return JsonParser.parseString(jsonString) + .getAsJsonObject() + .getAsJsonArray("issue") + .get(0) + .getAsJsonObject() + .getAsJsonPrimitive("diagnostics") + .getAsString(); + } catch (Exception e) { + return ""; + } + } + /** * Reports the progress of HTTP POST calls and the current thread pool size. *

@@ -217,9 +268,13 @@ private static Callable createPostCallable(HttpPost post, PostComponent po * and pool size information is printed to the standard output. */ private static void reportProgress() { - int currentCounter = counter.incrementAndGet(); - double percentage = (double) currentCounter / tasks.size() * 100; - System.out.print("\rPOST calls: " + String.format("%.2f%%", percentage) + " processed. Thread pool size: " + runningPostTaskList.size() + " "); + int currentCounter = processedPostCounter++; + double percentage = (double) currentCounter / getTotalTaskCount() * 100; + System.out.print("\rPOST calls: " + String.format("%.2f%%", percentage) + " processed. POST response pool size: " + runningPostTaskList.size() + ". "); + } + + private static int getTotalTaskCount() { + return tasks.size() + initialTasks.size(); } /** @@ -240,40 +295,27 @@ public static void postTaskCollection() { ExecutorService executorService = Executors.newFixedThreadPool(1); try { - System.out.println(tasks.size() + " POST calls to be made. Starting now. Please wait..."); + System.out.println(getTotalTaskCount() + " POST calls to be made. Starting now. Please wait..."); double percentage = 0; System.out.print("\rPOST: " + String.format("%.2f%%", percentage) + " done. "); - List> futures = new ArrayList<>(); - List resources = new ArrayList<>(tasks.keySet()); - for (int i = 0; i < resources.size(); i++) { - IBaseResource thisResource = resources.get(i); - if (runningPostTaskList.size() < MAX_SIMULTANEOUS_POST_COUNT) { - runningPostTaskList.add(thisResource); - futures.add(executorService.submit(tasks.get(thisResource))); - } else { - threadSleep(10); - i--; - } - } + //execute any tasks marked as having priority: + executeTasks(executorService, initialTasks); - for (Future future : futures) { - try { - future.get(); - } catch (Exception e) { - logger.error("HTTPClientUtils future.get()", e); - } - } + //execute the remaining tasks: + executeTasks(executorService, tasks); + + reportProgress(); System.out.println("Processing results..."); - successfulPostCalls.sort(postResultMessageComparator); + Collections.sort(successfulPostCalls); StringBuilder message = new StringBuilder(); - message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); for (String successPost : successfulPostCalls) { message.append("\n").append(successPost); } - System.out.println(message.toString()); + message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); + System.out.println(message); successfulPostCalls = new ArrayList<>(); if (!failedPostCalls.isEmpty()) { @@ -286,23 +328,23 @@ public static void postTaskCollection() { cleanUp(); //clear the queue, reset the counter, start fresh for (Pair pair : failedPostCallList) { - PostComponent postPojo = pair.getRight(); + PostComponent postComponent = pair.getRight(); try { - post(postPojo.fhirServerUrl, postPojo.resource, postPojo.encoding, postPojo.fhirContext); + post(postComponent.fhirServerUrl, + postComponent.resource, + postComponent.encoding, + postComponent.fhirContext, + postComponent.fileLocation, + postComponent.hasPriority); } catch (IOException e) { throw new RuntimeException(e); } } + //execute any tasks marked as having priority: + executeTasks(executorService, initialTasks); - for (Callable task : tasks.values()) { - try { - task.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - threadSleep(50); - } - + //execute the remaining tasks: + executeTasks(executorService, tasks); if (failedPostCalls.isEmpty()) { System.out.println("Retry successful, all tasks successfully posted"); } @@ -311,27 +353,27 @@ public static void postTaskCollection() { if (!successfulPostCalls.isEmpty()) { message = new StringBuilder(); - message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); for (String successPost : successfulPostCalls) { message.append("\n").append(successPost); } - System.out.println(message.toString()); + message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); + System.out.println(message); successfulPostCalls = new ArrayList<>(); } if (!failedPostCalls.isEmpty()) { - System.out.println("\n" + failedPostCalls.size() + " task(s) still failed to POST: "); List failedMessages = new ArrayList<>(); for (Pair pair : failedPostCalls) { failedMessages.add(pair.getLeft()); } - failedMessages.sort(postResultMessageComparator); + Collections.sort(failedMessages); message = new StringBuilder(); - message.append("\r\n").append(failedMessages.size()).append(" resources failed to post."); + for (String failedPost : failedMessages) { message.append("\n").append(failedPost); } - System.out.println(message.toString()); + message.append("\r\n").append(failedMessages.size()).append(" resources failed to post."); + System.out.println(message); } } finally { @@ -340,21 +382,44 @@ public static void postTaskCollection() { } } + private static void executeTasks(ExecutorService executorService, Map> executableTasksMap) { + List> futures = new ArrayList<>(); + List resources = new ArrayList<>(executableTasksMap.keySet()); + for (int i = 0; i < resources.size(); i++) { + String thisResourceId = resources.get(i); + if (runningPostTaskList.size() < MAX_SIMULTANEOUS_POST_COUNT) { + runningPostTaskList.add(thisResourceId); + futures.add(executorService.submit(executableTasksMap.get(thisResourceId))); + } else { + threadSleep(10); + i--; + } + } + + for (Future future : futures) { + try { + future.get(); + } catch (Exception e) { + logger.error("HTTPClientUtils future.get()", e); + } + } + } + /** * Pauses the current thread's execution for a specified duration. - * * This method causes the current thread to sleep for the given duration, allowing a pause in execution. If an * interruption occurs during the sleep, it is logged as an exception. * * @param i The duration, in milliseconds, for which the thread should sleep. */ - private static void threadSleep(int i){ + private static void threadSleep(int i) { try { Thread.sleep(i); } catch (InterruptedException e) { logger.error("postTaskCollection", new RuntimeException(e)); } } + /** * Cleans up and resets internal data structures after processing HTTP POST tasks. *

@@ -371,7 +436,8 @@ private static void cleanUp() { failedPostCalls = new ConcurrentLinkedQueue<>(); successfulPostCalls = new CopyOnWriteArrayList<>(); tasks = new ConcurrentHashMap<>(); - counter.set(0); + initialTasks = new ConcurrentHashMap<>(); + processedPostCounter = 0; runningPostTaskList = new CopyOnWriteArrayList<>(); } @@ -404,41 +470,19 @@ private static class PostComponent { IBaseResource resource; IOUtils.Encoding encoding; FhirContext fhirContext; + String fileLocation; + boolean hasPriority; - public PostComponent(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext) { + public PostComponent(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext, String fileLocation, boolean hasPriority) { this.fhirServerUrl = fhirServerUrl; this.resource = resource; this.encoding = encoding; this.fhirContext = fhirContext; + this.fileLocation = fileLocation; + this.hasPriority = hasPriority; } } - /** - * Sorts a list by the initial numbers so that we see the [iteration] of [total] message - * in ascending order - **/ - private static final Comparator postResultMessageComparator = new Comparator<>() { - @Override - public int compare(String s1, String s2) { - int value1 = extractValue(s1); - int value2 = extractValue(s2); - return Integer.compare(value1, value2); - } - - private int extractValue(String s) { - String[] parts = s.split(" "); - if (parts.length > 0) { - try { - return Integer.parseInt(parts[0]); - } catch (NumberFormatException e) { - return 0; - } - } - return 0; - } - }; - - public static ResponseHandler getDefaultResponseHandler() { return response -> { int status = response.getStatusLine().getStatusCode(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java index 86109fa08..e979984f8 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java @@ -10,12 +10,12 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.cqframework.cql.cql2elm.*; -import org.cqframework.cql.elm.tracking.TrackBack; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.Utilities; -import org.opencds.cqf.tooling.cql.exception.CQLTranslatorException; +import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.library.LibraryProcessor; +import org.opencds.cqf.tooling.processor.CqlProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -144,8 +144,8 @@ public static void writeResource(T resource, String pa // Issue 96 // If includeVersion is false then just use name and not id for the file baseName // if (Boolean.FALSE.equals(versioned)) { - // Assumes that the id will be a string with - separating the version number - // baseName = baseName.split("-")[0]; + // Assumes that the id will be a string with - separating the version number + // baseName = baseName.split("-")[0]; // } outputPath = FilenameUtils.concat(path, formatFileName(baseName, encoding, fhirContext)); } @@ -196,11 +196,13 @@ public static void writeBundle(Object bundle, String path, Encoding encoding, Fh } } + private static final Map alreadyCopied = new HashMap<>(); public static int copyFileCounter() { - return copyFileCounter; + return persistCopyFileCounter; } - private static int copyFileCounter = 0; + private static int persistCopyFileCounter = 0; + public static boolean copyFile(String inputPath, String outputPath) { if ((inputPath == null || inputPath.isEmpty()) && @@ -229,10 +231,10 @@ public static boolean copyFile(String inputPath, String outputPath) { Path src = Paths.get(inputPath); Path dest = Paths.get(outputPath); Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING); - - if (inputPath.toLowerCase().contains("tests-")){ - copyFileCounter++; -// System.out.println("Total tests-*: " + testsCounter + ": " + inputPath); + String separator = System.getProperty("file.separator"); + if (inputPath.toLowerCase().contains(separator + "tests-") || + inputPath.toLowerCase().contains(separator + "group-")){ + persistCopyFileCounter++; } alreadyCopied.put(key, outputPath); @@ -246,6 +248,7 @@ public static boolean copyFile(String inputPath, String outputPath) { } + public static String getTypeQualifiedResourceId(String path, FhirContext fhirContext) { IBaseResource resource = readResource(path, fhirContext, true); if (resource != null) { @@ -383,7 +386,7 @@ public static List getFilePaths(String directoryPath, Boolean recursive) //note: this is not the same as ANDing recursive to isDirectory as that would result in directories // being added to the list if the request is not recursive. if (Boolean.TRUE.equals(recursive)) { - filePaths.addAll(getFilePaths(file.getPath(), recursive)); + filePaths.addAll(getFilePaths(file.getPath(), true)); } } else { filePaths.add(file.getPath()); @@ -521,7 +524,7 @@ public static Boolean pathEndsWithElement(String igPath, String pathElement) { return result; } - public static List getDependencyCqlPaths(String cqlContentPath, Boolean includeVersion) throws CQLTranslatorException{ + public static List getDependencyCqlPaths(String cqlContentPath, Boolean includeVersion) throws CqlTranslatorException { List dependencyFiles = getDependencyCqlFiles(cqlContentPath, includeVersion); List dependencyPaths = new ArrayList<>(); for (File file : dependencyFiles) { @@ -530,7 +533,7 @@ public static List getDependencyCqlPaths(String cqlContentPath, Boolean return dependencyPaths; } - public static List getDependencyCqlFiles(String cqlContentPath, Boolean includeVersion) throws CQLTranslatorException{ + public static List getDependencyCqlFiles(String cqlContentPath, Boolean includeVersion) throws CqlTranslatorException { File cqlContent = new File(cqlContentPath); File cqlContentDir = cqlContent.getParentFile(); if (!cqlContentDir.isDirectory()) { @@ -566,39 +569,29 @@ public static List getDependencyCqlFiles(String cqlContentPath, Boolean in } private static final Map cachedTranslator = new LinkedHashMap<>(); - public static CqlTranslator translate(String cqlContentPath, ModelManager modelManager, LibraryManager libraryManager, CqlTranslatorOptions options) throws CQLTranslatorException { + + public static CqlTranslator translate(File cqlFile, LibraryManager libraryManager) throws CqlTranslatorException { + String cqlContentPath = cqlFile.getAbsolutePath(); CqlTranslator translator = cachedTranslator.get(cqlContentPath); if (translator != null) { return translator; } try { - File cqlFile = new File(cqlContentPath); if (!cqlFile.getName().endsWith(".cql")) { - throw new CQLTranslatorException("cqlContentPath must be a path to a .cql file"); + throw new CqlTranslatorException("cqlContentPath must be a path to a .cql file"); } translator = CqlTranslator.fromFile(cqlFile, libraryManager); - if (!translator.getErrors().isEmpty()) { - throw new CQLTranslatorException(listTranslatorErrors(translator)); + if (CqlProcessor.hasSevereErrors(translator.getErrors())) { + throw new CqlTranslatorException(translator.getErrors()); } + cachedTranslator.put(cqlContentPath, translator); return translator; } catch (IOException e) { - throw new CQLTranslatorException(e); - } - } - - private static ArrayList listTranslatorErrors(CqlTranslator translator) { - ArrayList errors = new ArrayList<>(); - for (CqlCompilerException error : translator.getErrors()) { - TrackBack tb = error.getLocator(); - String lines = tb == null ? "[n/a]" : String.format("[%d:%d, %d:%d]", - tb.getStartLine(), tb.getStartChar(), tb.getEndLine(), tb.getEndChar()); - //System.err.printf("%s %s%n", lines, error.getMessage()); - errors.add(lines + error.getMessage()); + throw new CqlTranslatorException(e); } - return errors; } public static String getCqlString(String cqlContentPath) { @@ -1042,12 +1035,15 @@ public static void clearDevicePaths() { } private static void setupDevicePaths(FhirContext fhirContext) { - devicePaths = new LinkedHashSet<>(); - HashMap resources = new LinkedHashMap<>(); + devicePaths = new LinkedHashSet <>(); + Map resources = new LinkedHashMap<>(); for (String dir : resourceDirectories) { for(String path : IOUtils.getFilePaths(dir, true)) { try { - resources.put(path, IOUtils.readResource(path, fhirContext, true)); + IBaseResource resource = IOUtils.readResource(path, fhirContext, true); + if (resource != null) { + resources.put(path, resource); + } } catch (Exception e) { if(path.toLowerCase().contains("device")) { logger.error("Error reading in Device from path: {} \n {}", path, e.getMessage()); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java index 442328872..0240d9fb8 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java @@ -19,12 +19,7 @@ import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.Validate; -import org.cqframework.cql.cql2elm.CqlTranslator; -import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.CqlTranslatorOptionsMapper; -import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; -import org.cqframework.cql.cql2elm.LibraryManager; -import org.cqframework.cql.cql2elm.ModelManager; +import org.cqframework.cql.cql2elm.*; import org.cqframework.cql.cql2elm.quick.FhirLibrarySourceProvider; import org.hl7.elm.r1.IncludeDef; import org.hl7.elm.r1.ValueSetDef; @@ -36,7 +31,7 @@ import org.hl7.fhir.instance.model.api.ICompositeType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.CanonicalType; -import org.opencds.cqf.tooling.cql.exception.CQLTranslatorException; +import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.processor.ValueSetsProcessor; import org.opencds.cqf.tooling.utilities.IOUtils.Encoding; import org.slf4j.Logger; @@ -291,7 +286,7 @@ public static List getTerminologyDependencies(IBaseResource resource, Fh } } - public static Map getDepValueSetResources(String cqlContentPath, String igPath, FhirContext fhirContext, boolean includeDependencies, Boolean includeVersion) throws CQLTranslatorException { + public static Map getDepValueSetResources(String cqlContentPath, String igPath, FhirContext fhirContext, boolean includeDependencies, Boolean includeVersion) throws CqlTranslatorException { Map valueSetResources = new HashMap<>(); List valueSetDefIDs = getDepELMValueSetDefIDs(cqlContentPath); @@ -315,18 +310,17 @@ public static Map getDepValueSetResources(String cqlConte } if (dependencies.size() != valueSetResources.size()) { - List missingValueSets = new ArrayList<>(); - dependencies.removeAll(valueSetResources.keySet()); - for (String valueSetUrl : dependencies) { - missingValueSets.add(valueSetUrl + " MISSING"); - } - logger.error(missingValueSets.toString()); - throw new CQLTranslatorException(missingValueSets); + List missingValueSets = new ArrayList<>(); + dependencies.removeAll(valueSetResources.keySet()); + for (String valueSetUrl : dependencies) { + missingValueSets.add(valueSetUrl + " MISSING"); + } + throw new CqlTranslatorException(missingValueSets, CqlCompilerException.ErrorSeverity.Warning); } return valueSetResources; } - public static List getIncludedLibraryNames(String cqlContentPath, Boolean includeVersion) throws CQLTranslatorException{ + public static List getIncludedLibraryNames(String cqlContentPath, Boolean includeVersion) throws CqlTranslatorException { List includedLibraryNames = new ArrayList<>(); List includedDefs = getIncludedDefs(cqlContentPath); for (IncludeDef def : includedDefs) { @@ -336,7 +330,7 @@ public static List getIncludedLibraryNames(String cqlContentPath, Boolea return includedLibraryNames; } - public static List getDepELMValueSetDefIDs(String cqlContentPath) throws CQLTranslatorException { + public static List getDepELMValueSetDefIDs(String cqlContentPath) throws CqlTranslatorException { List includedValueSetDefIDs = new ArrayList<>(); List valueSetDefs = getValueSetDefs(cqlContentPath); for (ValueSetDef def : valueSetDefs) { @@ -345,7 +339,7 @@ public static List getDepELMValueSetDefIDs(String cqlContentPath) throws return includedValueSetDefIDs; } - public static List getIncludedDefs(String cqlContentPath) throws CQLTranslatorException{ + public static List getIncludedDefs(String cqlContentPath) throws CqlTranslatorException { ArrayList includedDefs = new ArrayList<>(); org.hl7.elm.r1.Library elm = getElmFromCql(cqlContentPath); if (elm.getIncludes() != null && !elm.getIncludes().getDef().isEmpty()) { @@ -354,7 +348,7 @@ public static List getIncludedDefs(String cqlContentPath) throws CQL return includedDefs; } - public static List getValueSetDefs(String cqlContentPath) throws CQLTranslatorException{ + public static List getValueSetDefs(String cqlContentPath) throws CqlTranslatorException { ArrayList valueSetDefs = new ArrayList<>(); org.hl7.elm.r1.Library elm; elm = getElmFromCql(cqlContentPath); @@ -373,52 +367,73 @@ public static CqlTranslatorOptions getTranslatorOptions(String folder) { logger.debug("cql-options loaded from: {}", file.getAbsolutePath()); } else { - options = CqlTranslatorOptions.defaultOptions(); - if (!options.getFormats().contains(CqlTranslatorOptions.Format.XML)) { - options.getFormats().add(CqlTranslatorOptions.Format.XML); - } - logger.debug("cql-options not found. Using default options."); + options = CqlTranslatorOptions.defaultOptions(); + if (!options.getFormats().contains(CqlTranslatorOptions.Format.XML)) { + options.getFormats().add(CqlTranslatorOptions.Format.XML); + } + logger.debug("cql-options not found. Using default options."); } return options; } - private static Map cachedElm = new HashMap<>(); - public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) throws CQLTranslatorException { - org.hl7.elm.r1.Library elm = cachedElm.get(cqlContentPath); - if (elm != null) { - return elm; - } + public static CqlProcesses getCQLCqlTranslator(String cqlContentPath) throws CqlTranslatorException { + return getCQLCqlTranslator(new File(cqlContentPath)); + } + public static CqlProcesses getCQLCqlTranslator(File file) throws CqlTranslatorException { + String cqlContentPath = file.getAbsolutePath(); String folder = IOUtils.getParentDirectoryPath(cqlContentPath); - - CqlTranslatorOptions options = getTranslatorOptions(folder); - - // Setup - // Construct DefaultLibrarySourceProvider - // Construct FhirLibrarySourceProvider + CqlTranslatorOptions options = ResourceUtils.getTranslatorOptions(folder); ModelManager modelManager = new ModelManager(); LibraryManager libraryManager = new LibraryManager(modelManager); - // if (packages != null) { - // libraryManager.getLibrarySourceLoader().registerProvider(new NpmLibrarySourceProvider(packages, reader, logger)); - // } libraryManager.getLibrarySourceLoader().registerProvider(new FhirLibrarySourceProvider()); libraryManager.getLibrarySourceLoader().registerProvider(new DefaultLibrarySourceProvider(Paths.get(folder))); + return new CqlProcesses(options, modelManager, libraryManager, IOUtils.translate(file, libraryManager)); + } - // loadNamespaces(libraryManager); + public static class CqlProcesses { + CqlTranslatorOptions options; + ModelManager modelManager; + LibraryManager libraryManager; + CqlTranslator translator; - // foreach *.cql file - // for (File file : new File(folder).listFiles(getCqlFilenameFilter())) { - // translateFile(modelManager, libraryManager, file, options); - // } + public CqlProcesses(CqlTranslatorOptions options, + ModelManager modelManager, + LibraryManager libraryManager, + CqlTranslator translator) { + this.options = options; + this.modelManager = modelManager; + this.libraryManager = libraryManager; + this.translator = translator; + } + public CqlTranslatorOptions getOptions() { + return options; + } - // ModelManager modelManager = new ModelManager(); - // GenericLibrarySourceProvider sourceProvider = new GenericLibrarySourceProvider(folder); - // LibraryManager libraryManager = new LibraryManager(modelManager); - // libraryManager.getLibrarySourceLoader().registerProvider(sourceProvider); + public ModelManager getModelManager() { + return modelManager; + } + + public LibraryManager getLibraryManager() { + return libraryManager; + } + + public CqlTranslator getTranslator() { + return translator; + } - CqlTranslator translator = IOUtils.translate(cqlContentPath, modelManager, libraryManager, options); + + } + + private static Map cachedElm = new HashMap<>(); + public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) throws CqlTranslatorException { + org.hl7.elm.r1.Library elm = cachedElm.get(cqlContentPath); + if (elm != null) { + return elm; + } + CqlTranslator translator = getCQLCqlTranslator(cqlContentPath).getTranslator(); elm = translator.toELM(); cachedElm.put(cqlContentPath, elm); return elm; @@ -835,49 +850,49 @@ public static BaseRuntimeElementDefinition getElementDefinition(FhirContext f return fhirContext.getElementDefinition(elementName); } - //to keep track of output already written and avoid duplicate functionality slowing down performance: - private static ConcurrentHashMap outputResourceTracker = new ConcurrentHashMap<>(); - public static final String separator = System.getProperty("file.separator"); - public static void outputResource(IBaseResource resource, String encoding, FhirContext context, String outputPath) { - String resourceFileLocation = outputPath + separator + - resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + - "." + encoding; - if (outputResourceTracker.containsKey(resource.getIdElement().getResourceType() + ":" + outputPath)){ - LogUtils.info("This resource has already been processed: " + resource.getIdElement().getResourceType()); - return; - } - - try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + "." + encoding)) { - writer.write( - encoding.equals("json") - ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - ); - writer.flush(); - outputResourceTracker.put(resourceFileLocation, Boolean.TRUE); - - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e.getMessage()); - } - } - - public static void outputResourceByName(IBaseResource resource, String encoding, FhirContext context, String outputPath, String name) { - try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) { - writer.write( - encoding.equals("json") - ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() - ); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e.getMessage()); - } - } - - public static void cleanUp(){ - outputResourceTracker = new ConcurrentHashMap<>(); - cachedElm = new HashMap(); - } + //to keep track of output already written and avoid duplicate functionality slowing down performance: + private static ConcurrentHashMap outputResourceTracker = new ConcurrentHashMap<>(); + public static final String separator = System.getProperty("file.separator"); + public static void outputResource(IBaseResource resource, String encoding, FhirContext context, String outputPath) { + String resourceFileLocation = outputPath + separator + + resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + + "." + encoding; + if (outputResourceTracker.containsKey(resource.getIdElement().getResourceType() + ":" + outputPath)){ + LogUtils.info("This resource has already been processed: " + resource.getIdElement().getResourceType()); + return; + } + + try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + resource.getIdElement().getResourceType() + "-" + resource.getIdElement().getIdPart() + "." + encoding)) { + writer.write( + encoding.equals("json") + ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + ); + writer.flush(); + outputResourceTracker.put(resourceFileLocation, Boolean.TRUE); + + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } + + public static void outputResourceByName(IBaseResource resource, String encoding, FhirContext context, String outputPath, String name) { + try (FileOutputStream writer = new FileOutputStream(outputPath + "/" + name + "." + encoding)) { + writer.write( + encoding.equals("json") + ? context.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + : context.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource).getBytes() + ); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e.getMessage()); + } + } + + public static void cleanUp(){ + outputResourceTracker = new ConcurrentHashMap<>(); + cachedElm = new HashMap(); + } } diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java index e13a5a155..bce7b1487 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java @@ -25,16 +25,10 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.IniFile; import org.opencds.cqf.tooling.RefreshTest; -import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.measure.MeasureProcessor; import org.opencds.cqf.tooling.parameter.RefreshIGParameters; -import org.opencds.cqf.tooling.processor.CDSHooksProcessor; -import org.opencds.cqf.tooling.processor.IGBundleProcessor; import org.opencds.cqf.tooling.processor.IGProcessor; -import org.opencds.cqf.tooling.processor.PlanDefinitionProcessor; import org.opencds.cqf.tooling.processor.TestCaseProcessor; import org.opencds.cqf.tooling.processor.argument.RefreshIGArgumentProcessor; -import org.opencds.cqf.tooling.questionnaire.QuestionnaireProcessor; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.ResourceUtils; import org.slf4j.Logger; @@ -75,180 +69,179 @@ public RefreshIGOperationTest() { private final PrintStream originalStdOut = System.out; private ByteArrayOutputStream console = new ByteArrayOutputStream(); - @BeforeClass - public void init() { - // This overrides the default max string length for Jackson (which wiremock uses under the hood). - var constraints = StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build(); - Json.getObjectMapper().getFactory().setStreamReadConstraints(constraints); - } - - @BeforeMethod - public void setUp() throws Exception { - IOUtils.resourceDirectories = new ArrayList(); - IOUtils.clearDevicePaths(); - System.setOut(new PrintStream(this.console)); - File dir = new File("target" + separator + "refreshIG"); - if (dir.exists()) { - FileUtils.deleteDirectory(dir); - } + @BeforeClass + public void init() { + // This overrides the default max string length for Jackson (which wiremock uses under the hood). + var constraints = StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build(); + Json.getObjectMapper().getFactory().setStreamReadConstraints(constraints); + } + + @BeforeMethod + public void setUp() throws Exception { + IOUtils.resourceDirectories = new ArrayList(); + IOUtils.clearDevicePaths(); + System.setOut(new PrintStream(this.console)); + File dir = new File("target" + separator + "refreshIG"); + if (dir.exists()) { + FileUtils.deleteDirectory(dir); + } deleteTempINI(); - } - - /** - * This test breaks down refreshIG's process and can verify multiple bundles - */ - @SuppressWarnings("unchecked") - @Test - //TODO: Fix separately, this is blocking a bunch of other higher priority things - public void testBundledFiles() throws IOException { - //we can assert how many bundles were posted by keeping track via WireMockServer - //first find an open port: - int availablePort = findAvailablePort(); - String fhirUri = "http://localhost:" + availablePort + "/fhir"; - if (availablePort == -1){ - fhirUri = ""; - logger.info("No available ports to test post with. Removing mock fhir server from test."); - }else{ - System.out.println("Available port: " + availablePort + ", mock fhir server url: " + fhirUri); - } - - WireMockServer wireMockServer = null; - if (!fhirUri.isEmpty()) { - wireMockServer = new WireMockServer(availablePort); - wireMockServer.start(); - - WireMock.configureFor("localhost", availablePort); - wireMockServer.stubFor(WireMock.post(WireMock.urlEqualTo("/fhir")) - .willReturn(WireMock.aResponse() - .withStatus(200) - .withBody("Mock response"))); - } - - // Call the method under test, which should use HttpClientUtils.post - copyResourcesToTargetDir("target" + separator + "refreshIG", "testfiles/refreshIG"); - // build ini object - File iniFile = new File(INI_LOC); - String iniFileLocation = iniFile.getAbsolutePath(); - IniFile ini = new IniFile(iniFileLocation); - - String bundledFilesLocation = iniFile.getParent() + separator + "bundles" + separator + "measure" + separator; - - String[] args; - if (!fhirUri.isEmpty()) { - args = new String[]{"-RefreshIG", "-ini=" + INI_LOC, "-t", "-d", "-p", "-e=json", "-ts=false", "-fs=" + fhirUri}; - } else { - args = new String[]{"-RefreshIG", "-ini=" + INI_LOC, "-t", "-d", "-p", "-e=json", "-ts=false"}; - } - - // EXECUTE REFRESHIG WITH OUR ARGS: - new RefreshIGOperation().execute(args); - - int requestCount = WireMock.getAllServeEvents().size(); - assertEquals(requestCount, 3); //Looking for 3 resources posts (measure, and two tests) - - if (wireMockServer != null) { - wireMockServer.stop(); - } - - // determine fhireContext for measure lookup - FhirContext fhirContext = IGProcessor.getIgFhirContext(getFhirVersion(ini)); - - // get list of measures resulting from execution - Map measures = IOUtils.getMeasures(fhirContext); - - // loop through measure, verify each has all resources from multiple files - // bundled into single file using id/resourceType as lookup: - for (String measureName : measures.keySet()) { - // location of single bundled file: - final String bundledFileResult = bundledFilesLocation + measureName + separator + measureName - + "-bundle.json"; - // multiple individual files in sub directory to loop through: - final Path dir = Paths - .get(bundledFilesLocation + separator + measureName + separator + measureName + "-files"); - - // loop through each file, determine resourceType and treat accordingly - Map resourceTypeMap = new HashMap<>(); - - try (final DirectoryStream dirStream = Files.newDirectoryStream(dir)) { - dirStream.forEach(path -> { - File file = new File(path.toString()); - - if (file.getName().toLowerCase().endsWith(".json")) { - - Map map = this.jsonMap(file); - if (map == null) { - System.out.println("# Unable to parse " + file.getName() + " as json"); - } else { - - // ensure "resourceType" exists - if (map.containsKey(RESOURCE_TYPE)) { - String parentResourceType = (String) map.get(RESOURCE_TYPE); - // if Library, resource will be translated into "Measure" in main bundled file: - if (parentResourceType.equalsIgnoreCase(LIB_TYPE)) { - resourceTypeMap.put((String) map.get(ID), MEASURE_TYPE); - } else if (parentResourceType.equalsIgnoreCase(BUNDLE_TYPE)) { - // file is a bundle type, loop through resources in entry list, build up map of - // : - if (map.get(ENTRY) != null) { - ArrayList> entryList = (ArrayList>) map.get(ENTRY); - for (Map entry : entryList) { - if (entry.containsKey(RESOURCE)) { - Map resourceMap = (Map) entry.get(RESOURCE); - resourceTypeMap.put((String) resourceMap.get(ID), - (String) resourceMap.get(RESOURCE_TYPE)); - } - } - } - } - } - } - } - }); - - } catch (IOException e) { - logger.info(e.getMessage()); - } - - // map out entries in the resulting single bundle file: - Map bundledJson = this.jsonMap(new File(bundledFileResult)); - Map bundledJsonResourceTypes = new HashMap<>(); - ArrayList> entryList = (ArrayList>) bundledJson.get(ENTRY); - for (Map entry : entryList) { - Map resourceMap = (Map) entry.get(RESOURCE); - bundledJsonResourceTypes.put((String) resourceMap.get(ID), (String) resourceMap.get(RESOURCE_TYPE)); - } - - // compare mappings of to ensure all bundled correctly: - assertTrue(mapsAreEqual(resourceTypeMap, bundledJsonResourceTypes)); - } + } + + /** + * This test breaks down refreshIG's process and can verify multiple bundles + */ + @SuppressWarnings("unchecked") + @Test + //TODO: Fix separately, this is blocking a bunch of other higher priority things + public void testBundledFiles() throws IOException { + //we can assert how many bundles were posted by keeping track via WireMockServer + //first find an open port: + int availablePort = findAvailablePort(); + String fhirUri = "http://localhost:" + availablePort + "/fhir/"; + if (availablePort == -1){ + fhirUri = ""; + logger.info("No available ports to test post with. Removing mock fhir server from test."); + }else{ + System.out.println("Available port: " + availablePort + ", mock fhir server url: " + fhirUri); + } + + WireMockServer wireMockServer = null; + if (!fhirUri.isEmpty()) { + wireMockServer = new WireMockServer(availablePort); + wireMockServer.start(); + + WireMock.configureFor("localhost", availablePort); + wireMockServer.stubFor(WireMock.post(WireMock.urlPathMatching("/fhir/([a-zA-Z]*)")) + .willReturn(WireMock.aResponse() + .withStatus(200) + .withBody("Mock response"))); + } + + // Call the method under test, which should use HttpClientUtils.post + copyResourcesToTargetDir("target" + separator + "refreshIG", "testfiles/refreshIG"); + // build ini object + File iniFile = new File(INI_LOC); + String iniFileLocation = iniFile.getAbsolutePath(); + IniFile ini = new IniFile(iniFileLocation); + + String bundledFilesLocation = iniFile.getParent() + separator + "bundles" + separator + "measure" + separator; + + String[] args; + if (!fhirUri.isEmpty()) { + args = new String[]{"-RefreshIG", "-ini=" + INI_LOC, "-t", "-d", "-p", "-e=json", "-ts=false", "-fs=" + fhirUri}; + } else { + args = new String[]{"-RefreshIG", "-ini=" + INI_LOC, "-t", "-d", "-p", "-e=json", "-ts=false"}; + } + + // EXECUTE REFRESHIG WITH OUR ARGS: + new RefreshIGOperation().execute(args); + + int requestCount = WireMock.getAllServeEvents().size(); + assertEquals(requestCount, 6); //Looking for 6 resources posted (all files found in -files ending in .cql, .xml, or .json) + + if (wireMockServer != null) { + wireMockServer.stop(); + } + + // determine fhireContext for measure lookup + FhirContext fhirContext = IGProcessor.getIgFhirContext(getFhirVersion(ini)); + + // get list of measures resulting from execution + Map measures = IOUtils.getMeasures(fhirContext); + + // loop through measure, verify each has all resources from multiple files + // bundled into single file using id/resourceType as lookup: + for (String measureName : measures.keySet()) { + // location of single bundled file: + final String bundledFileResult = bundledFilesLocation + measureName + separator + measureName + + "-bundle.json"; + // multiple individual files in sub directory to loop through: + final Path dir = Paths + .get(bundledFilesLocation + separator + measureName + separator + measureName + "-files"); + + // loop through each file, determine resourceType and treat accordingly + Map resourceTypeMap = new HashMap<>(); + + try (final DirectoryStream dirStream = Files.newDirectoryStream(dir)) { + dirStream.forEach(path -> { + File file = new File(path.toString()); + + if (file.getName().toLowerCase().endsWith(".json")) { + + Map map = this.jsonMap(file); + if (map == null) { + System.out.println("# Unable to parse " + file.getName() + " as json"); + } else { + + // ensure "resourceType" exists + if (map.containsKey(RESOURCE_TYPE)) { + String parentResourceType = (String) map.get(RESOURCE_TYPE); + // if Library, resource will be translated into "Measure" in main bundled file: + if (parentResourceType.equalsIgnoreCase(LIB_TYPE)) { + resourceTypeMap.put((String) map.get(ID), MEASURE_TYPE); + } else if (parentResourceType.equalsIgnoreCase(BUNDLE_TYPE)) { + // file is a bundle type, loop through resources in entry list, build up map of + // : + if (map.get(ENTRY) != null) { + ArrayList> entryList = (ArrayList>) map.get(ENTRY); + for (Map entry : entryList) { + if (entry.containsKey(RESOURCE)) { + Map resourceMap = (Map) entry.get(RESOURCE); + resourceTypeMap.put((String) resourceMap.get(ID), + (String) resourceMap.get(RESOURCE_TYPE)); + } + } + } + } + } + } + } + }); + + } catch (IOException e) { + logger.info(e.getMessage()); + } + + // map out entries in the resulting single bundle file: + Map bundledJson = this.jsonMap(new File(bundledFileResult)); + Map bundledJsonResourceTypes = new HashMap<>(); + ArrayList> entryList = (ArrayList>) bundledJson.get(ENTRY); + for (Map entry : entryList) { + Map resourceMap = (Map) entry.get(RESOURCE); + bundledJsonResourceTypes.put((String) resourceMap.get(ID), (String) resourceMap.get(RESOURCE_TYPE)); + } + + // compare mappings of to ensure all bundled correctly: + assertTrue(mapsAreEqual(resourceTypeMap, bundledJsonResourceTypes)); + } // run cleanup (maven runs all ci tests sequentially and static member variables could retain values from previous tests) IOUtils.cleanUp(); ResourceUtils.cleanUp(); - TestCaseProcessor.cleanUp(); - } - - private static int findAvailablePort() { - for (int port = 8000; port <= 9000; port++) { - if (isPortAvailable(port)) { - return port; - } - } - return -1; - } - - private static boolean isPortAvailable(int port) { - ServerSocket ss; - try (ServerSocket serverSocket = new ServerSocket(port)) { - System.out.println("Trying " + serverSocket); - ss = serverSocket; - } catch (IOException e) { - return false; - } - System.out.println(ss + " is open."); - return true; - } + } + + private static int findAvailablePort() { + for (int port = 8000; port <= 9000; port++) { + if (isPortAvailable(port)) { + return port; + } + } + return -1; + } + + private static boolean isPortAvailable(int port) { + ServerSocket ss; + try (ServerSocket serverSocket = new ServerSocket(port)) { + System.out.println("Trying " + serverSocket); + ss = serverSocket; + } catch (IOException e) { + return false; + } + System.out.println(ss + " is open."); + return true; + } //@Test(expectedExceptions = IllegalArgumentException.class) //TODO: Fix separately, this is blocking a bunch of other higher priority things @@ -336,45 +329,37 @@ public void testParamsMissingINI() { File iniFile = this.createTempINI(igProperties); - String args[] = { "-RefreshIG", "-ini=" + iniFile.getAbsolutePath(), "-t", "-d", "-p" }; + String[] args = { "-RefreshIG", "-ini=" + iniFile.getAbsolutePath(), "-t", "-d", "-p" }; + + RefreshIGParameters params = null; + try { + params = new RefreshIGArgumentProcessor().parseAndConvert(args); + } + catch (Exception e) { + System.err.println(e.getMessage()); + System.exit(1); + } + + //override ini to be null + params.ini = null; - RefreshIGParameters params = null; - try { - params = new RefreshIGArgumentProcessor().parseAndConvert(args); - } - catch (Exception e) { - System.err.println(e.getMessage()); - System.exit(1); - } - MeasureProcessor measureProcessor = new MeasureProcessor(); - LibraryProcessor libraryProcessor = new LibraryProcessor(); - CDSHooksProcessor cdsHooksProcessor = new CDSHooksProcessor(); - PlanDefinitionProcessor planDefinitionProcessor = new PlanDefinitionProcessor(libraryProcessor, cdsHooksProcessor); - QuestionnaireProcessor questionnaireProcessor = new QuestionnaireProcessor(libraryProcessor); - IGBundleProcessor igBundleProcessor = new IGBundleProcessor(measureProcessor, planDefinitionProcessor, questionnaireProcessor); - IGProcessor processor = new IGProcessor(igBundleProcessor, libraryProcessor, measureProcessor); - - //override ini to be null - params.ini = null; - - - try { - processor.publishIG(params); + try { + new IGProcessor().publishIG(params); } catch (Exception e) { assertEquals(e.getClass(), NullPointerException.class); } - deleteTempINI(); + deleteTempINI(); } - @AfterMethod - public void afterTest() { + @AfterMethod + public void afterTest() { deleteTempINI(); - System.setOut(this.originalStdOut); - System.out.println(this.console.toString()); - this.console = new ByteArrayOutputStream(); - } + System.setOut(this.originalStdOut); + System.out.println(this.console.toString()); + this.console = new ByteArrayOutputStream(); + } private File createTempINI(Map properties) { diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/processor/IGProcessorTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/processor/IGProcessorTest.java index 7bf8b0ef7..8024f5b5b 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/processor/IGProcessorTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/processor/IGProcessorTest.java @@ -25,10 +25,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.IniFile; import org.opencds.cqf.tooling.RefreshTest; -import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.measure.MeasureProcessor; import org.opencds.cqf.tooling.parameter.RefreshIGParameters; -import org.opencds.cqf.tooling.questionnaire.QuestionnaireProcessor; import org.opencds.cqf.tooling.utilities.IOUtils; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -40,7 +37,6 @@ public class IGProcessorTest extends RefreshTest { - private final IGProcessor processor; private final ByteArrayOutputStream console = new ByteArrayOutputStream(); private final String ID = "id"; @@ -55,13 +51,6 @@ public class IGProcessorTest extends RefreshTest { public IGProcessorTest() { super(FhirContext.forCached(FhirVersionEnum.R4), "IGProcessorTest"); - LibraryProcessor libraryProcessor = new LibraryProcessor(); - MeasureProcessor measureProcessor = new MeasureProcessor(); - CDSHooksProcessor cdsHooksProcessor = new CDSHooksProcessor(); - PlanDefinitionProcessor planDefinitionProcessor = new PlanDefinitionProcessor(libraryProcessor, cdsHooksProcessor); - QuestionnaireProcessor questionnaireProcessor = new QuestionnaireProcessor(libraryProcessor); - IGBundleProcessor igBundleProcessor = new IGBundleProcessor(measureProcessor, planDefinitionProcessor, questionnaireProcessor); - processor = new IGProcessor(igBundleProcessor, libraryProcessor, measureProcessor); } @BeforeMethod @@ -97,7 +86,7 @@ void testRefreshIG() throws Exception { params.versioned = false; params.shouldApplySoftwareSystemStamp = true; params.addBundleTimestamp = true; //setting this true to test timestamp added in generated bundle - processor.publishIG(params); + new IGProcessor().publishIG(params); // determine fhireContext for measure lookup FhirContext fhirContext = IGProcessor.getIgFhirContext(getFhirVersion(ini)); diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessorTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessorTest.java index aa7b640bf..7a1d163b7 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessorTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/processor/PlanDefinitionProcessorTest.java @@ -4,7 +4,7 @@ public class PlanDefinitionProcessorTest { - private PlanDefinitionProcessor processor; + private PlanDefinitionBundler processor; @Test private void testBundlePlanDefinitions() throws Exception { diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessorTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessorTest.java index 0c1ca331f..4704e504d 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessorTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireProcessorTest.java @@ -5,9 +5,7 @@ import org.apache.commons.io.FileUtils; import org.opencds.cqf.tooling.RefreshTest; import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.quick.QuickPageGenerator; import org.opencds.cqf.tooling.utilities.IOUtils; -import org.opencds.cqf.tooling.utilities.LogUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.BeforeMethod; @@ -24,12 +22,12 @@ public class QuestionnaireProcessorTest extends RefreshTest { private final String TARGET_PATH = "target" + separator + "bundleQuestionnaires"; private final String INI_PATH = TARGET_PATH + separator + "ig.ini"; private ByteArrayOutputStream console = new ByteArrayOutputStream(); - private QuestionnaireProcessor questionnaireProcessor; + private QuestionnaireBundler questionnaireProcessor; public QuestionnaireProcessorTest() { super(FhirContext.forCached(FhirVersionEnum.R4), "QuestionnaireProcessorTest"); LibraryProcessor libraryProcessor = new LibraryProcessor(); - questionnaireProcessor = new QuestionnaireProcessor(libraryProcessor); + questionnaireProcessor = new QuestionnaireBundler(libraryProcessor); } @BeforeMethod @@ -42,7 +40,7 @@ public void setUp() throws Exception { } } - public QuestionnaireProcessorTest(QuestionnaireProcessor questionnaireProcessor, FhirContext fhirContext) { + public QuestionnaireProcessorTest(QuestionnaireBundler questionnaireProcessor, FhirContext fhirContext) { super(fhirContext); this.questionnaireProcessor = questionnaireProcessor; } @@ -80,7 +78,7 @@ private void testBundleQuestionnairesR4() throws Exception { "libraryevaluationtest" + separator + "libraryevaluationtest-bundle.json"; questionnaireProcessor.bundleResources(refreshedLibraryNames, TARGET_PATH, binaryPaths, includeDependencies, includeTerminology, - includePatientScenarios, versioned, addBundleTimestamp, fhirContext, null, IOUtils.Encoding.JSON); + includePatientScenarios, versioned, addBundleTimestamp, fhirContext, null, IOUtils.Encoding.JSON, true); File outputBundleFile = new File(outputBundleFilePath); From 93c4982dfb069111a4682ee817fcea117e3ab695 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Wed, 13 Dec 2023 22:43:20 -0500 Subject: [PATCH 02/20] Tracking POST tasks via IBaseResource as key rather than resourceID to avoid possible duplicate keys --- .../cqf/tooling/measure/MeasureBundler.java | 5 +- .../cqf/tooling/measure/MeasureProcessor.java | 2 +- .../tooling/operation/RefreshIGOperation.java | 2 +- .../parameter/RefreshIGParameters.java | 2 +- .../parameter/RefreshMeasureParameters.java | 2 +- .../tooling/processor/AbstractBundler.java | 79 ++++++++++++------- .../cqf/tooling/processor/BaseProcessor.java | 11 ++- .../cqf/tooling/processor/CqlProcessor.java | 16 ++-- .../tooling/processor/IGBundleProcessor.java | 20 ++--- .../cqf/tooling/processor/IGProcessor.java | 8 +- .../tooling/processor/IGTestProcessor.java | 2 +- .../tooling/processor/IProcessorContext.java | 2 +- .../processor/PlanDefinitionBundler.java | 4 +- .../tooling/processor/TestCaseProcessor.java | 19 +++-- .../argument/RefreshIGArgumentProcessor.java | 8 +- .../questionnaire/QuestionnaireBundler.java | 4 +- .../tooling/utilities/HttpClientUtils.java | 40 +++++----- .../cqf/tooling/utilities/IOUtils.java | 22 ++---- .../operation/RefreshIGOperationTest.java | 2 +- .../org/opencds/cqf/tooling/r4/ReadMe.md | 2 +- 20 files changed, 124 insertions(+), 128 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java index 14342e0f2..10c2d95cb 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java @@ -43,12 +43,12 @@ protected Set getPaths(FhirContext fhirContext) { //so far only the Measure Bundle process needs to persist extra files: @Override - protected List persistExtraFiles(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri) { + protected int persistFilesFolder(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri) { //persist tests-* before group-* files and make a record of which files were tracked: List persistedFiles = persistTestFilesWithPriority(bundleDestPath, libraryName, encoding, fhirContext, fhirUri); persistedFiles.addAll(persistEverythingElse(bundleDestPath, libraryName, encoding, fhirContext, fhirUri, persistedFiles)); - return persistedFiles; + return persistedFiles.size(); } private List persistTestFilesWithPriority(String bundleDestPath, String libraryName, Encoding encoding, FhirContext fhirContext, String fhirUri) { @@ -57,7 +57,6 @@ private List persistTestFilesWithPriority(String bundleDestPath, String File directory = new File(filesLoc); if (directory.exists()) { File[] filesInDir = directory.listFiles(); - if (!(filesInDir == null || filesInDir.length == 0)) { for (File file : filesInDir) { if (file.getName().toLowerCase().startsWith("tests-")) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java index bcd72826e..7093bbd7a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java @@ -113,7 +113,7 @@ private Measure refreshGeneratedContent(Measure measure, MeasureRefreshProcessor List errors = new CopyOnWriteArrayList<>(); CompiledLibrary CompiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors); - System.out.println(CqlProcessor.buildStatusMessage(errors, measure.getName(), includeErrors)); + System.out.println(CqlProcessor.buildStatusMessage(errors, measure.getName(), verboseMessaging)); boolean hasSevereErrors = CqlProcessor.hasSevereErrors(errors); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java index ea69ec9a6..7708f3269 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java @@ -26,7 +26,7 @@ public void execute(String[] args) { System.exit(1); } - if (params.includeErrors == null || !params.includeErrors) { + if (params.verboseMessaging == null || !params.verboseMessaging) { System.out.println("\r\nRe-run with -x to for expanded reporting of errors, warnings, and informational messages.\r\n"); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java index d65f61ca8..e46d272ef 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshIGParameters.java @@ -24,5 +24,5 @@ public class RefreshIGParameters { public String libraryPath; public String libraryOutputPath; public String measureOutputPath; - public Boolean includeErrors; + public Boolean verboseMessaging; } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java index 624191b08..4c098c0f6 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/parameter/RefreshMeasureParameters.java @@ -51,5 +51,5 @@ The path to the measure resource(s) */ public String measureOutputDirectory; - public Boolean includeErrors; + public Boolean verboseMessaging; } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index a7934bf7a..e32690e8f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -17,6 +17,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; /** * An abstract base class for bundlers that handle the bundling of various types of resources within an ig. @@ -110,9 +111,8 @@ private String getResourcePrefix() { */ public void bundleResources(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean includeVersion, Boolean addBundleTimestamp, - FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean includeErrors) { + FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean verboseMessaging) { - final Map resourcesMap = getResources(fhirContext); final List bundledResources = new CopyOnWriteArrayList<>(); //for keeping track of progress: @@ -121,13 +121,17 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa //for keeping track of failed reasons: final Map failedExceptionMessages = new ConcurrentHashMap<>(); + //keeping track of error list returned during cql translation: final Map> cqlTranslatorErrorMessages = new ConcurrentHashMap<>(); - //build list of tasks via for loop: + //used to summarize file count user can expect to see in POST queue for each resource: + final Map persistedFileReport = new ConcurrentHashMap<>(); + + //build list of executable tasks to be sent to thread pool: List> tasks = new ArrayList<>(); - try { - final StringBuilder persistedFileReport = new StringBuilder(); + try { + final Map resourcesMap = getResources(fhirContext); final Map libraryUrlMap = IOUtils.getLibraryUrlMap(fhirContext); final Map libraries = IOUtils.getLibraries(fhirContext); final Map libraryPathMap = IOUtils.getLibraryPathMap(fhirContext); @@ -241,33 +245,34 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa primaryLibrarySourcePath, fhirContext, encoding, includeTerminology, includeDependencies, includePatientScenarios, includeVersion, addBundleTimestamp, cqlTranslatorErrorMessages); - //Child classes implement any extra processing of files (Such as MeasureBundler persisting tests-*) - List persistedExtraFiles = persistExtraFiles(bundleDestPath, resourceName, encoding, fhirContext, fhirUri); + //If user supplied a fhir server url, inform them of total # of files to be persisted to the server: + if (fhirUri != null && !fhirUri.isEmpty()) { + persistedFileReport.put(resourceName, + //+1 to account for -bundle + persistFilesFolder(bundleDestPath, resourceName, encoding, fhirContext, fhirUri) + 1); + } if (cdsHooksProcessor != null) { List activityDefinitionPaths = CDSHooksProcessor.bundleActivityDefinitions(resourceSourcePath, fhirContext, resources, encoding, includeVersion, shouldPersist); cdsHooksProcessor.addActivityDefinitionFilesToBundle(igPath, bundleDestPath, activityDefinitionPaths, fhirContext, encoding); } - //If user supplied a fhir server url, inform them of total # of files to be persisted to the server: - if (fhirUri != null && !fhirUri.isEmpty()) { - persistedFileReport.append("\r\n") - //all persisted files + the bundle: - .append(persistedExtraFiles.size() + 1) - .append(" total files will be posted to ") - .append(fhirUri) - .append(" for ") - .append(resourceName); - } bundledResources.add(resourceSourcePath); } } catch (Exception e) { - if (resourceSourcePath == null) { - failedExceptionMessages.put(resourceEntry.getValue().getIdElement().getIdPart(), e.getMessage()); + String failMsg = ""; + if (e.getMessage() != null ){ + failMsg = e.getMessage(); + }else{ + failMsg = e.getClass().getName(); + e.printStackTrace(); + } + if (resourceSourcePath == null || resourceSourcePath.isEmpty()) { + failedExceptionMessages.put(resourceEntry.getValue().getIdElement().getIdPart(), failMsg); } else { - failedExceptionMessages.put(resourceSourcePath, e.getMessage()); + failedExceptionMessages.put(resourceSourcePath, failMsg); } } @@ -282,17 +287,33 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa ThreadUtils.executeTasks(tasks); - if (!persistedFileReport.toString().isEmpty()) { - System.out.println(persistedFileReport); - } - - } catch (Exception e) { LogUtils.putException("bundleResources: " + getResourceProcessorType(), e); } + //Prepare final report: + StringBuilder message = new StringBuilder(NEWLINE); + + //Give user a snapshot of the files each resource will have persisted to their FHIR server (if fhirUri is provided) + if (!persistedFileReport.isEmpty()) { + message.append(NEWLINE).append(persistedFileReport.size()).append(" ").append(getResourceProcessorType()).append("(s) have POST tasks in the queue:"); + int totalQueueCount = 0; + for (String library : persistedFileReport.keySet()) { + totalQueueCount = totalQueueCount + persistedFileReport.get(library); + message.append(NEWLINE_INDENT) + .append(library) + .append(": ") + .append(persistedFileReport.get(library)) + .append(" File(s) will be posted to ") + .append(fhirUri); + } + message.append(NEWLINE_INDENT) + .append("Total: ") + .append(totalQueueCount) + .append(" File(s)"); + } - StringBuilder message = new StringBuilder(NEWLINE + bundledResources.size() + " " + getResourceProcessorType() + "(s) successfully bundled:"); + message.append(NEWLINE).append(bundledResources.size()).append(" ").append(getResourceProcessorType()).append("(s) successfully bundled:"); for (String bundledResource : bundledResources) { message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); } @@ -327,12 +348,12 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa for (String library : cqlTranslatorErrorMessages.keySet()) { message.append(INDENT).append( - CqlProcessor.buildStatusMessage(cqlTranslatorErrorMessages.get(library), library, includeErrors, false, NEWLINE_INDENT2) + CqlProcessor.buildStatusMessage(cqlTranslatorErrorMessages.get(library), library, verboseMessaging, false, NEWLINE_INDENT2) ).append(NEWLINE); } } - System.out.println(message.toString()); + System.out.println(message); } @@ -360,7 +381,7 @@ private void persistBundle(String bundleDestPath, String libraryName, } - protected abstract List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri); + protected abstract int persistFilesFolder(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri); private void bundleFiles(String igPath, String bundleDestPath, String primaryLibraryName, List binaryPaths, String resourceFocusSourcePath, String librarySourcePath, FhirContext fhirContext, IOUtils.Encoding encoding, Boolean includeTerminology, Boolean includeDependencies, Boolean includePatientScenarios, diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java index 8d3861c43..ed1d68203 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java @@ -75,11 +75,10 @@ public NpmPackageManager getPackageManager() { protected IProcessorContext parentContext; - //used to inform user of errors that occurred during refresh of library, measure, or test cases - public Boolean includeErrors = false; + public Boolean verboseMessaging = false; - public Boolean getIncludeErrors() { - return includeErrors; + public Boolean getVerboseMessaging() { + return verboseMessaging; } public void initialize(IProcessorContext context) { @@ -95,7 +94,7 @@ public void initialize(IProcessorContext context) { this.packageManager = parentContext.getPackageManager(); this.binaryPaths = parentContext.getBinaryPaths(); this.cqlProcessor = parentContext.getCqlProcessor(); - this.includeErrors = parentContext.getIncludeErrors(); + this.verboseMessaging = parentContext.getVerboseMessaging(); } } @@ -170,7 +169,7 @@ public CqlProcessor getCqlProcessor() { } cqlProcessor = new CqlProcessor(new CopyOnWriteArrayList<>(packageManager.getNpmList()), new CopyOnWriteArrayList<>(binaryPaths), reader, this, ucumService, - packageId, canonicalBase, includeErrors); + packageId, canonicalBase, verboseMessaging); } return cqlProcessor; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java index ccb56f9ff..14e67cbeb 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java @@ -135,9 +135,9 @@ public List getParameters() { private NamespaceInfo namespaceInfo; - private boolean includeErrors; + private boolean verboseMessaging; - public CqlProcessor(List packages, List folders, ILibraryReader reader, ILoggingService logger, UcumService ucumService, String packageId, String canonicalBase, Boolean includeErrors) { + public CqlProcessor(List packages, List folders, ILibraryReader reader, ILoggingService logger, UcumService ucumService, String packageId, String canonicalBase, Boolean verboseMessaging) { super(); this.packages = packages; this.folders = folders; @@ -149,7 +149,7 @@ public CqlProcessor(List packages, List folders, ILibraryRea if (packageId != null && !packageId.isEmpty() && canonicalBase != null && !canonicalBase.isEmpty()) { this.namespaceInfo = new NamespaceInfo(packageId, canonicalBase); } - this.includeErrors = includeErrors; + this.verboseMessaging = verboseMessaging; } /** @@ -409,7 +409,7 @@ private void translateFile(LibraryManager libraryManager, File file, CqlCompiler } //output Success/Warn/Info/Fail message to user: - System.out.println(buildStatusMessage(translator.getErrors(), file.getName(), includeErrors)); + System.out.println(buildStatusMessage(translator.getErrors(), file.getName(), verboseMessaging)); } catch (Exception e) { result.getErrors().add(new ValidationMessage(ValidationMessage.Source.Publisher, IssueType.EXCEPTION, file.getName(), "CQL Processing failed with exception: "+e.getMessage(), IssueSeverity.ERROR)); @@ -449,11 +449,11 @@ private static List listBySeverity(List errors, String resourceName, boolean includeErrors){ - return buildStatusMessage(errors, resourceName, includeErrors, true, "\n\t"); + public static String buildStatusMessage(List errors, String resourceName, boolean verboseMessaging){ + return buildStatusMessage(errors, resourceName, verboseMessaging, true, "\n\t"); } - public static String buildStatusMessage(List errors, String resourceName, boolean includeErrors, boolean withStatusIndicator, String delimiter){ + public static String buildStatusMessage(List errors, String resourceName, boolean verboseMessaging, boolean withStatusIndicator, String delimiter){ String successMsg = "[SUCCESS] CQL Processing of "; String statusIndicatorMinor = " completed successfully"; String statusIndicator; @@ -490,7 +490,7 @@ public static String buildStatusMessage(List errors, Strin String warningStatus = warningsList.size() + " Warning(s)" ; return (withStatusIndicator ? statusIndicator : "") + "CQL Processing of " + resourceName + statusIndicatorMinor + " with " + errorsStatus + ", " - + warningStatus + ", and " + infoStatus + (includeErrors ? ": " + delimiter + fullSortedListMsg : ""); + + warningStatus + ", and " + infoStatus + (verboseMessaging ? ": " + delimiter + fullSortedListMsg : ""); } public static boolean hasSevereErrors(List errors) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java index 7432f8f70..4dd80b96d 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java @@ -20,12 +20,12 @@ public class IGBundleProcessor { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String bundleFilesPathElement = "files/"; - private Boolean includeErrors = true; + private Boolean verboseMessaging = true; LibraryProcessor libraryProcessor; CDSHooksProcessor cdsHooksProcessor; - public IGBundleProcessor(Boolean includeErrors, LibraryProcessor libraryProcessor, CDSHooksProcessor cdsHooksProcessor) { - this.includeErrors = includeErrors; + public IGBundleProcessor(Boolean verboseMessaging, LibraryProcessor libraryProcessor, CDSHooksProcessor cdsHooksProcessor) { + this.verboseMessaging = verboseMessaging; this.libraryProcessor = libraryProcessor; this.cdsHooksProcessor = cdsHooksProcessor; } @@ -40,14 +40,8 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis new MeasureBundler().bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding, includeErrors); - - //this message can be moved to any point of this process, but so far it's just the bundle measure process - //that will persist test files. If Questionnaires and PlanDefinitions should ever need test files as well - //persistTestFiles can be moved to AbstractResourceProcessor from MeasureProcessor instead of abstract sig - System.out.println("\r\nTotal test files copied: " + IOUtils.copyFileCounter() + ". " + - (fhirUri != null && !fhirUri.isEmpty() ? "These files will be posted to " + fhirUri : "") - ); + fhirUri, encoding, verboseMessaging); + System.out.println("\r\n[Bundle Measures has finished - " + getTime() + "]\r\n"); @@ -55,7 +49,7 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis new PlanDefinitionBundler(this.libraryProcessor, this.cdsHooksProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding, includeErrors); + fhirUri, encoding, verboseMessaging); System.out.println("\r\n[Bundle PlanDefinitions has finished - " + getTime() + "]\r\n"); @@ -64,7 +58,7 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis new QuestionnaireBundler(this.libraryProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, - fhirUri, encoding, includeErrors); + fhirUri, encoding, verboseMessaging); System.out.println("\r\n[Bundle Questionnaires has finished - " + getTime() + "]\r\n"); 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 82afc58b3..52accfcf1 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 @@ -52,7 +52,7 @@ public void publishIG(RefreshIGParameters params) { boolean igPathProvided = params.igPath != null && !params.igPath.isEmpty(); //presence of -x arg means give error details instead of just error count during cql processing - includeErrors = (params.includeErrors != null ? params.includeErrors : false); + verboseMessaging = (params.verboseMessaging != null ? params.verboseMessaging : false); if (!iniProvided && (!rootDirProvided || !igPathProvided)) { throw new IllegalArgumentException("Either the ini argument or both igPath and rootDir must be provided"); @@ -87,7 +87,6 @@ public void publishIG(RefreshIGParameters params) { //Use case 2 while developing in Atom refresh content and run tests for either entire IG or targeted Artifact //refreshContent - LogUtils.info("IGProcessor.publishIG - refreshIG"); refreshIG(params); //validate //ValidateProcessor.validate(ValidateParameters); @@ -96,11 +95,10 @@ public void publishIG(RefreshIGParameters params) { //Use case 3 //package everything - LogUtils.info("IGProcessor.publishIG - bundleIg"); Boolean skipPackages = params.skipPackages; if (!skipPackages) { - new IGBundleProcessor(params.includeErrors, new LibraryProcessor(), new CDSHooksProcessor()).bundleIg( + new IGBundleProcessor(params.verboseMessaging, new LibraryProcessor(), new CDSHooksProcessor()).bundleIg( refreshedResourcesNames, rootDir, getBinaryPaths(), @@ -172,7 +170,7 @@ public void refreshIG(RefreshIGParameters params) { if (includePatientScenarios) { TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); - testCaseProcessor.refreshTestCases(FilenameUtils.concat(rootDir, IGProcessor.TEST_CASE_PATH_ELEMENT), encoding, fhirContext, refreshedResourcesNames, includeErrors); + testCaseProcessor.refreshTestCases(FilenameUtils.concat(rootDir, IGProcessor.TEST_CASE_PATH_ELEMENT), encoding, fhirContext, refreshedResourcesNames, verboseMessaging); } } 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 dba5aeed6..009f290a6 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 @@ -183,7 +183,7 @@ public void testIg(TestIGParameters params) { System.out.println("\r\n[Refreshing Test Cases]\r\n"); TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); - testCaseProcessor.refreshTestCases(params.testCasesPath, IOUtils.Encoding.JSON, fhirContext, includeErrors); + testCaseProcessor.refreshTestCases(params.testCasesPath, IOUtils.Encoding.JSON, fhirContext, verboseMessaging); List TestResults = new ArrayList(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java index 57b0c782f..5456fd6c3 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IProcessorContext.java @@ -25,5 +25,5 @@ public interface IProcessorContext { CqlProcessor getCqlProcessor(); - Boolean getIncludeErrors(); + Boolean getVerboseMessaging(); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java index abd9334eb..7e0df5204 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java @@ -36,9 +36,9 @@ protected String getResourceProcessorType() { } @Override - protected List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { + protected int persistFilesFolder(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { //do nothing - return new ArrayList<>(); + return 0; } @Override diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index c0a5663a2..86dd128ac 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -29,12 +29,12 @@ public class TestCaseProcessor { public static final String separator = System.getProperty("file.separator"); private static final Logger logger = LoggerFactory.getLogger(TestCaseProcessor.class); - public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, Boolean includeErrors) { - refreshTestCases(path, encoding, fhirContext, null, includeErrors); + public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, Boolean verboseMessaging) { + refreshTestCases(path, encoding, fhirContext, null, verboseMessaging); } public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, @Nullable List refreshedResourcesNames, - Boolean includeErrors) { + Boolean verboseMessaging) { System.out.println("\r\n[Refreshing Tests]\r\n"); @@ -47,7 +47,6 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext IFhirVersion version = fhirContext.getVersion(); //build list of tasks via for loop: List> testGroupTasks = new ArrayList<>(); - ExecutorService testGroupExecutor = Executors.newCachedThreadPool(); List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); for (String group : resourceTypeTestGroups) { testGroupTasks.add(() -> { @@ -165,15 +164,15 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext return null; }); }//end for (String group : resourceTypeTestGroups) { - ThreadUtils.executeTasks(testGroupTasks, testGroupExecutor); + ThreadUtils.executeTasks(testGroupTasks); //Now with all possible tasks collected, progress can be reported instead of flooding the console. ThreadUtils.executeTasks(testCaseRefreshTasks); //ensure accurate progress at final stage: reportProgress((testCaseRefreshFailMap.size() + testCaseRefreshSuccessMap.size()), testCaseRefreshTasks.size()); - StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", includeErrors); + StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", verboseMessaging); if (!groupFileRefreshSuccessMap.isEmpty() || !groupFileRefreshFailMap.isEmpty()) { - testCaseMessage.append(buildInformationMessage(groupFileRefreshFailMap, groupFileRefreshSuccessMap, "Group File", "Created", includeErrors)); + testCaseMessage.append(buildInformationMessage(groupFileRefreshFailMap, groupFileRefreshSuccessMap, "Group File", "Created", verboseMessaging)); } System.out.println(testCaseMessage); } @@ -185,10 +184,10 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext * @param successMap which items succeeded * @param type group file or test case * @param successType created or refreshed - * @param includeErrors give the exception message if includeErrors is on + * @param verboseMessaging give the exception message if verboseMessaging is on * @return built message for console */ - private StringBuilder buildInformationMessage(Map failMap, Map successMap, String type, String successType, boolean includeErrors) { + private StringBuilder buildInformationMessage(Map failMap, Map successMap, String type, String successType, boolean verboseMessaging) { StringBuilder message = new StringBuilder(); if (!successMap.isEmpty() || !failMap.isEmpty()) { message.append(NEWLINE).append(successMap.size()).append(" ").append(type).append("(s) successfully ").append(successType.toLowerCase()).append(":"); @@ -198,7 +197,7 @@ private StringBuilder buildInformationMessage(Map failMap, Map ini = iniBuilder.withRequiredArg().describedAs("Path to the IG ini file"); OptionSpec rootDir = rootDirBuilder.withOptionalArg().describedAs("Root directory of the IG"); @@ -70,7 +70,7 @@ public OptionParser build() { OptionSpec measureOutputPath = measureOutputPathBuilder.withOptionalArg().describedAs("path to the output directory for updated measures"); OptionSpec shouldApplySoftwareSystemStamp = shouldApplySoftwareSystemStampBuilder.withOptionalArg().describedAs("Indicates whether refreshed Measure and Library resources should be stamped with the 'cqf-tooling' stamp via the cqfm-softwaresystem Extension"); OptionSpec shouldAddTimestampOptions = shouldAddTimestampBuilder.withOptionalArg().describedAs("Indicates whether refreshed Bundle should attach timestamp of creation"); - OptionSpec shouldIncludeErrorsOptions = shouldIncludeErrors.withOptionalArg().describedAs("Indicates that a complete list of errors during library, measure, and test case refresh are included upon failure."); + OptionSpec shouldVerboseMessagingOptions = shouldVerboseMessaging.withOptionalArg().describedAs("Indicates that a complete list of errors during library, measure, and test case refresh are included upon failure."); //TODO: FHIR user / password (and other auth options) @@ -149,7 +149,7 @@ public RefreshIGParameters parseAndConvert(String[] args) { addBundleTimestamp = true; } - Boolean includeErrors = options.has(SHOULD_INCLUDE_ERRORS[0]); + Boolean verboseMessaging = options.has(SHOULD_INCLUDE_ERRORS[0]); ArrayList paths = new ArrayList(); if (resourcePaths != null && !resourcePaths.isEmpty()) { @@ -178,7 +178,7 @@ public RefreshIGParameters parseAndConvert(String[] args) { ip.measureToRefreshPath = measureToRefreshPath; ip.libraryOutputPath = libraryOutputPath; ip.measureOutputPath = measureOutputPath; - ip.includeErrors = includeErrors; + ip.verboseMessaging = verboseMessaging; return ip; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java index 1862f737a..6dd85c315 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java @@ -36,9 +36,9 @@ protected String getResourceProcessorType() { } @Override - protected List persistExtraFiles(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { + protected int persistFilesFolder(String bundleDestPath, String libraryName, IOUtils.Encoding encoding, FhirContext fhirContext, String fhirUri) { //do nothing - return new ArrayList<>(); + return 0; } @Override diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index 7082e6d98..ea6acfc72 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -32,6 +32,12 @@ * A utility class for collecting HTTP requests to a FHIR server and executing them collectively. */ public class HttpClientUtils { + //60 second timeout + protected static final RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(60000) + .setConnectTimeout(60000) + .build(); + protected static final Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); private static final String FHIR_SERVER_URL = "FHIR Server URL"; private static final String BUNDLE_RESOURCE = "Bundle Resource"; @@ -46,9 +52,9 @@ public class HttpClientUtils { //failedPostCalls needs to maintain the details built in the FAILED message, as well as a copy of the inputs for a retry by the user on failed posts. private static Queue> failedPostCalls = new ConcurrentLinkedQueue<>(); private static List successfulPostCalls = new CopyOnWriteArrayList<>(); - private static Map> tasks = new ConcurrentHashMap<>(); - private static Map> initialTasks = new ConcurrentHashMap<>(); - private static List runningPostTaskList = new CopyOnWriteArrayList<>(); + private static Map> tasks = new ConcurrentHashMap<>(); + private static Map> initialTasks = new ConcurrentHashMap<>(); + private static List runningPostTaskList = new CopyOnWriteArrayList<>(); private static int processedPostCounter = 0; private HttpClientUtils() { @@ -133,9 +139,9 @@ private static void createPostTask(String fhirServerUrl, IBaseResource resource, PostComponent postPojo = new PostComponent(fhirServerUrl, resource, encoding, fhirContext, fileLocation, withPriority); HttpPost post = configureHttpPost(fhirServerUrl, resource, encoding, fhirContext); if (withPriority) { - initialTasks.put(resource.getIdElement().getIdPart(), createPostCallable(post, postPojo)); + initialTasks.put(resource, createPostCallable(post, postPojo)); } else { - tasks.put(resource.getIdElement().getIdPart(), createPostCallable(post, postPojo)); + tasks.put(resource, createPostCallable(post, postPojo)); } } catch (Exception e) { logger.error("Error while submitting the POST request: " + e.getMessage(), e); @@ -167,9 +173,7 @@ private static HttpPost configureHttpPost(String fhirServerUrl, IBaseResource re HttpPost post = new HttpPost(fhirServer); post.addHeader("content-type", "application/" + encoding.toString()); - String resourceString = IOUtils.encodeResourceAsString(resource, encoding, fhirContext); - StringEntity input; try { input = new StringEntity(resourceString); @@ -177,12 +181,6 @@ private static HttpPost configureHttpPost(String fhirServerUrl, IBaseResource re throw new RuntimeException(e); } post.setEntity(input); - - //60 second timeout - RequestConfig requestConfig = RequestConfig.custom() - .setSocketTimeout(60000) - .setConnectTimeout(60000) - .build(); post.setConfig(requestConfig); return post; @@ -209,7 +207,9 @@ private static Callable createPostCallable(HttpPost post, PostComponent po : postComponent.resource.getIdElement().getIdPart()); try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + HttpResponse response = httpClient.execute(post); + StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); String diagnosticString = getDiagnosticString(EntityUtils.toString(response.getEntity())); @@ -224,7 +224,7 @@ private static Callable createPostCallable(HttpPost post, PostComponent po failedPostCalls.add(Pair.of("[FAIL] Exception during " + resourceIdentifier + " POST request execution to " + postComponent.fhirServerUrl + ": " + e.getMessage(), postComponent)); } - runningPostTaskList.remove(postComponent.resource.getIdElement().getIdPart()); + runningPostTaskList.remove(postComponent.resource); reportProgress(); return null; }; @@ -242,8 +242,6 @@ private static Callable createPostCallable(HttpPost post, PostComponent po * } * It extracts the diagnostics and returns a string appendable to the response * - * @param jsonString - * @return */ private static String getDiagnosticString(String jsonString) { try { @@ -382,14 +380,14 @@ public static void postTaskCollection() { } } - private static void executeTasks(ExecutorService executorService, Map> executableTasksMap) { + private static void executeTasks(ExecutorService executorService, Map> executableTasksMap) { List> futures = new ArrayList<>(); - List resources = new ArrayList<>(executableTasksMap.keySet()); + List resources = new ArrayList<>(executableTasksMap.keySet()); for (int i = 0; i < resources.size(); i++) { - String thisResourceId = resources.get(i); + IBaseResource thisResource = resources.get(i); if (runningPostTaskList.size() < MAX_SIMULTANEOUS_POST_COUNT) { - runningPostTaskList.add(thisResourceId); - futures.add(executorService.submit(executableTasksMap.get(thisResourceId))); + runningPostTaskList.add(thisResource); + futures.add(executorService.submit(executableTasksMap.get(thisResource))); } else { threadSleep(10); i--; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java index e979984f8..ed080682e 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java @@ -198,54 +198,42 @@ public static void writeBundle(Object bundle, String path, Encoding encoding, Fh private static final Map alreadyCopied = new HashMap<>(); - public static int copyFileCounter() { - return persistCopyFileCounter; - } - private static int persistCopyFileCounter = 0; - public static boolean copyFile(String inputPath, String outputPath) { + public static void copyFile(String inputPath, String outputPath) { if ((inputPath == null || inputPath.isEmpty()) && (outputPath == null || outputPath.isEmpty())) { LogUtils.putException("IOUtils.copyFile", new IllegalArgumentException("IOUtils.copyFile: inputPath and outputPath are missing!")); - return false; + return; } if (inputPath == null || inputPath.isEmpty()) { LogUtils.putException("IOUtils.copyFile", new IllegalArgumentException("IOUtils.copyFile: inputPath missing!")); - return false; + return; } if (outputPath == null || outputPath.isEmpty()) { LogUtils.putException("IOUtils.copyFile", new IllegalArgumentException("IOUtils.copyFile: inputPath missing!")); - return false; + return; } String key = inputPath + ":" + outputPath; if (alreadyCopied.containsKey(key)) { // File already copied to destination, no need to do anything - return false; + return; } try { Path src = Paths.get(inputPath); Path dest = Paths.get(outputPath); Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING); - String separator = System.getProperty("file.separator"); - if (inputPath.toLowerCase().contains(separator + "tests-") || - inputPath.toLowerCase().contains(separator + "group-")){ - persistCopyFileCounter++; - } alreadyCopied.put(key, outputPath); - return true; } catch (IOException e) { logger.error(e.getMessage()); LogUtils.putException("IOUtils.copyFile(" + inputPath + ", " + outputPath + "): ", new RuntimeException("Error copying file: " + e.getMessage())); - return false; } - } diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java index bce7b1487..7c26478d6 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java @@ -139,7 +139,7 @@ public void testBundledFiles() throws IOException { new RefreshIGOperation().execute(args); int requestCount = WireMock.getAllServeEvents().size(); - assertEquals(requestCount, 6); //Looking for 6 resources posted (all files found in -files ending in .cql, .xml, or .json) + assertEquals(requestCount, 7); //Looking for 7 resources posted (all files found in -files ending in .cql, .xml, or .json) if (wireMockServer != null) { wireMockServer.stop(); diff --git a/tooling/src/test/resources/org/opencds/cqf/tooling/r4/ReadMe.md b/tooling/src/test/resources/org/opencds/cqf/tooling/r4/ReadMe.md index 9c58f6fe7..118f0beef 100644 --- a/tooling/src/test/resources/org/opencds/cqf/tooling/r4/ReadMe.md +++ b/tooling/src/test/resources/org/opencds/cqf/tooling/r4/ReadMe.md @@ -24,7 +24,7 @@ Refreshing the IG includes two steps: * a separate bundle for each set of associated Patient Scenario resources * a separate MeasureReport resource for each assocaited Patient Scenario (if present) -* The default behavior of running _refresh can be adjusted by changing the options in the _refresh file. One such usage is to post to a differnt FHIR Server (ex: local). To do so, modify the -fs (FHIR Server) option in _refresh. For more details on other options, run the CQF Tooling from the command line with -RefreshIG -help. +* The default behavior of running _refresh can be adjusted by changing the options in the _refresh file. One such usage is to post to a different FHIR Server (ex: local). To do so, modify the -fs (FHIR Server) option in _refresh. For more details on other options, run the CQF Tooling from the command line with -RefreshIG -help. ## Caveats: * If new ValueSet resources are posted to a CQF Ruler FHIR Server, they must be manually updated with {{serverurl}}$updateCodeSystems. Due to the potentially long duration of invoking $updateCodeSystems, the tooling does not run it as part of posting new ValueSet resources. From 1f6aa6528087d0281ad1935236ca4c6b87e40047 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 14 Dec 2023 08:17:13 -0500 Subject: [PATCH 03/20] Corrected formatting of cql processing summary. --- .../org/opencds/cqf/tooling/processor/AbstractBundler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index e32690e8f..b8ba007e6 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -347,9 +347,9 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceProcessorType()).append("(s) encountered CQL translator errors:"); for (String library : cqlTranslatorErrorMessages.keySet()) { - message.append(INDENT).append( + message.append(NEWLINE_INDENT).append( CqlProcessor.buildStatusMessage(cqlTranslatorErrorMessages.get(library), library, verboseMessaging, false, NEWLINE_INDENT2) - ).append(NEWLINE); + ); } } From ed0acb4e167a33baa801f4d6ec2fef5be1cddcd1 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 14 Dec 2023 14:08:47 -0500 Subject: [PATCH 04/20] Code cleanup. --- .../cqf/tooling/measure/MeasureProcessor.java | 4 +- .../cqf/tooling/utilities/ResourceUtils.java | 46 ++----------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java index 7093bbd7a..a1bae907a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java @@ -20,8 +20,8 @@ import java.util.concurrent.CopyOnWriteArrayList; public class MeasureProcessor extends BaseProcessor { - public static volatile String ResourcePrefix = "measure-"; - protected volatile List identifiers; + public static String ResourcePrefix = "measure-"; + protected List identifiers; public static String getId(String baseId) { return ResourcePrefix + baseId; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java index 0240d9fb8..f7f0f0db9 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/ResourceUtils.java @@ -377,54 +377,14 @@ public static CqlTranslatorOptions getTranslatorOptions(String folder) { return options; } - public static CqlProcesses getCQLCqlTranslator(String cqlContentPath) throws CqlTranslatorException { - return getCQLCqlTranslator(new File(cqlContentPath)); - } - - public static CqlProcesses getCQLCqlTranslator(File file) throws CqlTranslatorException { - String cqlContentPath = file.getAbsolutePath(); + public static CqlTranslator getCQLCqlTranslator(String cqlContentPath) throws CqlTranslatorException { String folder = IOUtils.getParentDirectoryPath(cqlContentPath); CqlTranslatorOptions options = ResourceUtils.getTranslatorOptions(folder); ModelManager modelManager = new ModelManager(); LibraryManager libraryManager = new LibraryManager(modelManager); libraryManager.getLibrarySourceLoader().registerProvider(new FhirLibrarySourceProvider()); libraryManager.getLibrarySourceLoader().registerProvider(new DefaultLibrarySourceProvider(Paths.get(folder))); - return new CqlProcesses(options, modelManager, libraryManager, IOUtils.translate(file, libraryManager)); - } - - public static class CqlProcesses { - CqlTranslatorOptions options; - ModelManager modelManager; - LibraryManager libraryManager; - CqlTranslator translator; - - public CqlProcesses(CqlTranslatorOptions options, - ModelManager modelManager, - LibraryManager libraryManager, - CqlTranslator translator) { - this.options = options; - this.modelManager = modelManager; - this.libraryManager = libraryManager; - this.translator = translator; - } - - public CqlTranslatorOptions getOptions() { - return options; - } - - public ModelManager getModelManager() { - return modelManager; - } - - public LibraryManager getLibraryManager() { - return libraryManager; - } - - public CqlTranslator getTranslator() { - return translator; - } - - + return IOUtils.translate(new File(cqlContentPath), libraryManager); } private static Map cachedElm = new HashMap<>(); @@ -433,7 +393,7 @@ public static org.hl7.elm.r1.Library getElmFromCql(String cqlContentPath) throws if (elm != null) { return elm; } - CqlTranslator translator = getCQLCqlTranslator(cqlContentPath).getTranslator(); + CqlTranslator translator = getCQLCqlTranslator(cqlContentPath); elm = translator.toELM(); cachedElm.put(cqlContentPath, elm); return elm; From cbff38d217d0f72e64fe8a57a5e6a37b646438bd Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 14 Dec 2023 15:07:23 -0500 Subject: [PATCH 05/20] users --- .../cqf/tooling/library/LibraryProcessor.java | 79 ++++++++----------- .../tooling/processor/AbstractBundler.java | 35 ++++---- .../tooling/processor/TestCaseProcessor.java | 19 ++--- 3 files changed, 53 insertions(+), 80 deletions(-) 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 7f0343480..a7141b114 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 @@ -45,13 +45,15 @@ public class LibraryProcessor extends BaseProcessor { private static final Logger logger = LoggerFactory.getLogger(LibraryProcessor.class); public static final String ResourcePrefix = "library-"; + public static String getId(String baseId) { return ResourcePrefix + baseId; } + private static Pattern pattern; private static Pattern getPattern() { - if(pattern == null) { + if (pattern == null) { String regex = "^[a-zA-Z]+[a-zA-Z0-9_\\-\\.]*"; pattern = Pattern.compile(regex); } @@ -59,7 +61,7 @@ private static Pattern getPattern() { } public static void validateIdAlphaNumeric(String id) { - if(!getPattern().matcher(id).find()) { + if (!getPattern().matcher(id).find()) { throw new RuntimeException("The library id format is invalid."); } } @@ -74,7 +76,6 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin public List refreshIgLibraryContent(BaseProcessor parentContext, Encoding outputEncoding, String libraryPath, String libraryOutputDirectory, Boolean versioned, FhirContext fhirContext, Boolean shouldApplySoftwareSystemStamp) { System.out.println("\r\n[Refreshing Libraries]\r\n"); - // ArrayList refreshedLibraryNames = new ArrayList(); LibraryProcessor libraryProcessor; switch (fhirContext.getVersion().getVersion()) { @@ -91,8 +92,7 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin if (libraryPath == null) { libraryPath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.LIBRARY_PATH_ELEMENT); - } - else if (!Utilities.isAbsoluteFileName(libraryPath)) { + } else if (!Utilities.isAbsoluteFileName(libraryPath)) { libraryPath = FilenameUtils.concat(parentContext.getRootDir(), libraryPath); } RefreshLibraryParameters params = new RefreshLibraryParameters(); @@ -114,64 +114,51 @@ else if (!Utilities.isAbsoluteFileName(libraryPath)) { * Bundles library dependencies for a given FHIR library file and populates the provided resource map. * This method executes asynchronously by invoking the associated task queue. * - * @param path The path to the FHIR library file. - * @param fhirContext The FHIR context to use for processing resources. - * @param resources The map to populate with library resources. - * @param encoding The encoding to use for reading and processing resources. - * @param versioned A boolean indicating whether to consider versioned resources. - * @return True if the bundling of library dependencies is successful; false otherwise. + * @param path The path to the FHIR library file. + * @param fhirContext The FHIR context to use for processing resources. + * @param resources The map to populate with library resources. + * @param encoding The encoding to use for reading and processing resources. + * @param versioned A boolean indicating whether to consider versioned resources. */ - public Boolean bundleLibraryDependencies(String path, FhirContext fhirContext, Map resources, - Encoding encoding, boolean versioned) { - try{ - Queue> bundleLibraryDependenciesTasks = bundleLibraryDependenciesTasks(path, fhirContext, resources, encoding, versioned); - ThreadUtils.executeTasks(bundleLibraryDependenciesTasks); - return true; - }catch (Exception e){ - return false; - } - + public void bundleLibraryDependencies(String path, FhirContext fhirContext, Map resources, + Encoding encoding, boolean versioned) throws Exception { + Queue> bundleLibraryDependenciesTasks = bundleLibraryDependenciesTasks(path, fhirContext, resources, encoding, versioned); + ThreadUtils.executeTasks(bundleLibraryDependenciesTasks); } /** * Recursively bundles library dependencies for a given FHIR library file and populates the provided resource map. * Each dependency is added as a Callable task to be executed asynchronously. * - * @param path The path to the FHIR library file. - * @param fhirContext The FHIR context to use for processing resources. - * @param resources The map to populate with library resources. - * @param encoding The encoding to use for reading and processing resources. - * @param versioned A boolean indicating whether to consider versioned resources. + * @param path The path to the FHIR library file. + * @param fhirContext The FHIR context to use for processing resources. + * @param resources The map to populate with library resources. + * @param encoding The encoding to use for reading and processing resources. + * @param versioned A boolean indicating whether to consider versioned resources. * @return A queue of Callable tasks, each representing the bundling of a library dependency. - * The Callable returns null (Void) and is meant for asynchronous execution. + * The Callable returns null (Void) and is meant for asynchronous execution. */ public Queue> bundleLibraryDependenciesTasks(String path, FhirContext fhirContext, Map resources, - Encoding encoding, boolean versioned) { + Encoding encoding, boolean versioned) throws Exception { Queue> returnTasks = new ConcurrentLinkedQueue<>(); String fileName = FilenameUtils.getName(path); boolean prefixed = fileName.toLowerCase().startsWith("library-"); - try { - Map dependencies = ResourceUtils.getDepLibraryResources(path, fhirContext, encoding, versioned, logger); - // String currentResourceID = IOUtils.getTypeQualifiedResourceId(path, fhirContext); - for (IBaseResource resource : dependencies.values()) { - returnTasks.add(() -> { - resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); + Map dependencies = ResourceUtils.getDepLibraryResources(path, fhirContext, encoding, versioned, logger); + // String currentResourceID = IOUtils.getTypeQualifiedResourceId(path, fhirContext); + for (IBaseResource resource : dependencies.values()) { + returnTasks.add(() -> { + resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); - // NOTE: Assuming dependency library will be in directory of dependent. - String dependencyPath = IOUtils.getResourceFileName(IOUtils.getResourceDirectory(path), resource, encoding, fhirContext, versioned, prefixed); + // NOTE: Assuming dependency library will be in directory of dependent. + String dependencyPath = IOUtils.getResourceFileName(IOUtils.getResourceDirectory(path), resource, encoding, fhirContext, versioned, prefixed); - returnTasks.addAll(bundleLibraryDependenciesTasks(dependencyPath, fhirContext, resources, encoding, versioned)); + returnTasks.addAll(bundleLibraryDependenciesTasks(dependencyPath, fhirContext, resources, encoding, versioned)); - //return statement needed for Callable - return null; - }); - } - } catch (Exception e) { - logger.error(path, e); - //purposely break addAll: - return null; + //return statement needed for Callable + return null; + }); } return returnTasks; } @@ -249,7 +236,7 @@ protected void setTranslatorOptions(Library sourceLibrary, CqlTranslatorOptions optionsReferenceValue = "#options"; optionsReference.setReference(optionsReferenceValue); } - Parameters optionsParameters = (Parameters)sourceLibrary.getContained(optionsReferenceValue); + Parameters optionsParameters = (Parameters) sourceLibrary.getContained(optionsReferenceValue); if (optionsParameters == null) { optionsParameters = new Parameters(); optionsParameters.setId(optionsReferenceValue.substring(1)); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index b8ba007e6..c971f5994 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -156,7 +157,6 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } final String resourceSourcePath = getSourcePath(fhirContext, resourceEntry); - tasks.add(() -> { //check if resourceSourcePath has been processed before: if (processedResources.contains(resourceSourcePath)) { @@ -167,7 +167,6 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa try { Map resources = new ConcurrentHashMap<>(); - Boolean shouldPersist = ResourceUtils.safeAddResource(resourceSourcePath, resources, fhirContext); if (!resources.containsKey(getResourceProcessorType() + "/" + resourceEntry.getKey())) { throw new IllegalArgumentException(String.format("Could not retrieve base resource for " + getResourceProcessorType() + " %s", resourceName)); @@ -216,24 +215,23 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa if (includeDependencies) { if (libraryProcessor == null) libraryProcessor = new LibraryProcessor(); - - boolean result = libraryProcessor.bundleLibraryDependencies(primaryLibrarySourcePath, fhirContext, resources, encoding, includeVersion); - if (shouldPersist && !result) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Library Dependency bundling failed."); + try { + libraryProcessor.bundleLibraryDependencies(primaryLibrarySourcePath, fhirContext, resources, encoding, includeVersion); + } catch (Exception bre) { + failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Library Dependency bundling failed: " + bre.getMessage()); //exit from task: return null; } - shouldPersist = shouldPersist & result; } if (includePatientScenarios) { - boolean result = TestCaseProcessor.bundleTestCases(igPath, getResourceTestGroupName(), primaryLibraryName, fhirContext, resources); - if (shouldPersist && !result) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Test Case bundling failed."); + try { + TestCaseProcessor.bundleTestCases(igPath, getResourceTestGroupName(), primaryLibraryName, fhirContext, resources); + } catch (Exception tce) { + failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Test Case bundling failed: " + tce.getMessage()); //exit from task: return null; } - shouldPersist = shouldPersist & result; } if (shouldPersist) { @@ -263,17 +261,12 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } catch (Exception e) { String failMsg = ""; - if (e.getMessage() != null ){ + if (e.getMessage() != null) { failMsg = e.getMessage(); - }else{ - failMsg = e.getClass().getName(); - e.printStackTrace(); - } - if (resourceSourcePath == null || resourceSourcePath.isEmpty()) { - failedExceptionMessages.put(resourceEntry.getValue().getIdElement().getIdPart(), failMsg); } else { - failedExceptionMessages.put(resourceSourcePath, failMsg); + failMsg = e.getClass().getName(); } + failedExceptionMessages.put(resourceSourcePath, failMsg); } processedResources.add(resourceSourcePath); @@ -321,7 +314,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa List resourcePathLibraryNames = new ArrayList<>(getPaths(fhirContext)); //gather which resources didn't make it - ArrayList failedResources = new ArrayList<>(resourcePathLibraryNames); + List failedResources = new ArrayList<>(resourcePathLibraryNames); resourcePathLibraryNames.removeAll(bundledResources); resourcePathLibraryNames.retainAll(refreshedLibraryNames); @@ -335,7 +328,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa failedResources.removeAll(resourcePathLibraryNames); message.append(NEWLINE).append(failedResources.size()).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); for (String failed : failedResources) { - if (failedExceptionMessages.containsKey(failed)) { + if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); } else { message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 86dd128ac..045af7d96 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -280,9 +280,8 @@ public static String getId(String baseId) { return "tests-" + baseId; } - public static Boolean bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, - Map resources) { - Boolean shouldPersist = true; + public static void bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, + Map resources) throws Exception { String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT), contextResourceType), libraryName); // this is breaking for bundle of a bundle. Replace with individual resources @@ -294,18 +293,12 @@ public static Boolean bundleTestCases(String igPath, String contextResourceType, // resources, fhirContext); // } - try { - List testCaseResources = TestCaseProcessor.getTestCaseResources(igTestCasePath, fhirContext); - for (IBaseResource resource : testCaseResources) { - if ((!(resource instanceof org.hl7.fhir.dstu3.model.Bundle)) && (!(resource instanceof org.hl7.fhir.r4.model.Bundle))) { - resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); - } + List testCaseResources = TestCaseProcessor.getTestCaseResources(igTestCasePath, fhirContext); + for (IBaseResource resource : testCaseResources) { + if ((!(resource instanceof org.hl7.fhir.dstu3.model.Bundle)) && (!(resource instanceof org.hl7.fhir.r4.model.Bundle))) { + resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); } - } catch (Exception e) { - shouldPersist = false; - logger.error(igTestCasePath, e); } - return shouldPersist; } From 03539ebb1d58a472351955bd686cc32d5efcd6fe Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 14 Dec 2023 15:07:23 -0500 Subject: [PATCH 06/20] Adjusted over to exception handling rather than boolean return for various methods during bundling process, to relay exception messages to uusers --- .../cqf/tooling/library/LibraryProcessor.java | 79 ++++++++----------- .../tooling/processor/AbstractBundler.java | 35 ++++---- .../tooling/processor/TestCaseProcessor.java | 19 ++--- 3 files changed, 53 insertions(+), 80 deletions(-) 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 7f0343480..a7141b114 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 @@ -45,13 +45,15 @@ public class LibraryProcessor extends BaseProcessor { private static final Logger logger = LoggerFactory.getLogger(LibraryProcessor.class); public static final String ResourcePrefix = "library-"; + public static String getId(String baseId) { return ResourcePrefix + baseId; } + private static Pattern pattern; private static Pattern getPattern() { - if(pattern == null) { + if (pattern == null) { String regex = "^[a-zA-Z]+[a-zA-Z0-9_\\-\\.]*"; pattern = Pattern.compile(regex); } @@ -59,7 +61,7 @@ private static Pattern getPattern() { } public static void validateIdAlphaNumeric(String id) { - if(!getPattern().matcher(id).find()) { + if (!getPattern().matcher(id).find()) { throw new RuntimeException("The library id format is invalid."); } } @@ -74,7 +76,6 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin public List refreshIgLibraryContent(BaseProcessor parentContext, Encoding outputEncoding, String libraryPath, String libraryOutputDirectory, Boolean versioned, FhirContext fhirContext, Boolean shouldApplySoftwareSystemStamp) { System.out.println("\r\n[Refreshing Libraries]\r\n"); - // ArrayList refreshedLibraryNames = new ArrayList(); LibraryProcessor libraryProcessor; switch (fhirContext.getVersion().getVersion()) { @@ -91,8 +92,7 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin if (libraryPath == null) { libraryPath = FilenameUtils.concat(parentContext.getRootDir(), IGProcessor.LIBRARY_PATH_ELEMENT); - } - else if (!Utilities.isAbsoluteFileName(libraryPath)) { + } else if (!Utilities.isAbsoluteFileName(libraryPath)) { libraryPath = FilenameUtils.concat(parentContext.getRootDir(), libraryPath); } RefreshLibraryParameters params = new RefreshLibraryParameters(); @@ -114,64 +114,51 @@ else if (!Utilities.isAbsoluteFileName(libraryPath)) { * Bundles library dependencies for a given FHIR library file and populates the provided resource map. * This method executes asynchronously by invoking the associated task queue. * - * @param path The path to the FHIR library file. - * @param fhirContext The FHIR context to use for processing resources. - * @param resources The map to populate with library resources. - * @param encoding The encoding to use for reading and processing resources. - * @param versioned A boolean indicating whether to consider versioned resources. - * @return True if the bundling of library dependencies is successful; false otherwise. + * @param path The path to the FHIR library file. + * @param fhirContext The FHIR context to use for processing resources. + * @param resources The map to populate with library resources. + * @param encoding The encoding to use for reading and processing resources. + * @param versioned A boolean indicating whether to consider versioned resources. */ - public Boolean bundleLibraryDependencies(String path, FhirContext fhirContext, Map resources, - Encoding encoding, boolean versioned) { - try{ - Queue> bundleLibraryDependenciesTasks = bundleLibraryDependenciesTasks(path, fhirContext, resources, encoding, versioned); - ThreadUtils.executeTasks(bundleLibraryDependenciesTasks); - return true; - }catch (Exception e){ - return false; - } - + public void bundleLibraryDependencies(String path, FhirContext fhirContext, Map resources, + Encoding encoding, boolean versioned) throws Exception { + Queue> bundleLibraryDependenciesTasks = bundleLibraryDependenciesTasks(path, fhirContext, resources, encoding, versioned); + ThreadUtils.executeTasks(bundleLibraryDependenciesTasks); } /** * Recursively bundles library dependencies for a given FHIR library file and populates the provided resource map. * Each dependency is added as a Callable task to be executed asynchronously. * - * @param path The path to the FHIR library file. - * @param fhirContext The FHIR context to use for processing resources. - * @param resources The map to populate with library resources. - * @param encoding The encoding to use for reading and processing resources. - * @param versioned A boolean indicating whether to consider versioned resources. + * @param path The path to the FHIR library file. + * @param fhirContext The FHIR context to use for processing resources. + * @param resources The map to populate with library resources. + * @param encoding The encoding to use for reading and processing resources. + * @param versioned A boolean indicating whether to consider versioned resources. * @return A queue of Callable tasks, each representing the bundling of a library dependency. - * The Callable returns null (Void) and is meant for asynchronous execution. + * The Callable returns null (Void) and is meant for asynchronous execution. */ public Queue> bundleLibraryDependenciesTasks(String path, FhirContext fhirContext, Map resources, - Encoding encoding, boolean versioned) { + Encoding encoding, boolean versioned) throws Exception { Queue> returnTasks = new ConcurrentLinkedQueue<>(); String fileName = FilenameUtils.getName(path); boolean prefixed = fileName.toLowerCase().startsWith("library-"); - try { - Map dependencies = ResourceUtils.getDepLibraryResources(path, fhirContext, encoding, versioned, logger); - // String currentResourceID = IOUtils.getTypeQualifiedResourceId(path, fhirContext); - for (IBaseResource resource : dependencies.values()) { - returnTasks.add(() -> { - resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); + Map dependencies = ResourceUtils.getDepLibraryResources(path, fhirContext, encoding, versioned, logger); + // String currentResourceID = IOUtils.getTypeQualifiedResourceId(path, fhirContext); + for (IBaseResource resource : dependencies.values()) { + returnTasks.add(() -> { + resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); - // NOTE: Assuming dependency library will be in directory of dependent. - String dependencyPath = IOUtils.getResourceFileName(IOUtils.getResourceDirectory(path), resource, encoding, fhirContext, versioned, prefixed); + // NOTE: Assuming dependency library will be in directory of dependent. + String dependencyPath = IOUtils.getResourceFileName(IOUtils.getResourceDirectory(path), resource, encoding, fhirContext, versioned, prefixed); - returnTasks.addAll(bundleLibraryDependenciesTasks(dependencyPath, fhirContext, resources, encoding, versioned)); + returnTasks.addAll(bundleLibraryDependenciesTasks(dependencyPath, fhirContext, resources, encoding, versioned)); - //return statement needed for Callable - return null; - }); - } - } catch (Exception e) { - logger.error(path, e); - //purposely break addAll: - return null; + //return statement needed for Callable + return null; + }); } return returnTasks; } @@ -249,7 +236,7 @@ protected void setTranslatorOptions(Library sourceLibrary, CqlTranslatorOptions optionsReferenceValue = "#options"; optionsReference.setReference(optionsReferenceValue); } - Parameters optionsParameters = (Parameters)sourceLibrary.getContained(optionsReferenceValue); + Parameters optionsParameters = (Parameters) sourceLibrary.getContained(optionsReferenceValue); if (optionsParameters == null) { optionsParameters = new Parameters(); optionsParameters.setId(optionsReferenceValue.substring(1)); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index b8ba007e6..c971f5994 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -156,7 +157,6 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } final String resourceSourcePath = getSourcePath(fhirContext, resourceEntry); - tasks.add(() -> { //check if resourceSourcePath has been processed before: if (processedResources.contains(resourceSourcePath)) { @@ -167,7 +167,6 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa try { Map resources = new ConcurrentHashMap<>(); - Boolean shouldPersist = ResourceUtils.safeAddResource(resourceSourcePath, resources, fhirContext); if (!resources.containsKey(getResourceProcessorType() + "/" + resourceEntry.getKey())) { throw new IllegalArgumentException(String.format("Could not retrieve base resource for " + getResourceProcessorType() + " %s", resourceName)); @@ -216,24 +215,23 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa if (includeDependencies) { if (libraryProcessor == null) libraryProcessor = new LibraryProcessor(); - - boolean result = libraryProcessor.bundleLibraryDependencies(primaryLibrarySourcePath, fhirContext, resources, encoding, includeVersion); - if (shouldPersist && !result) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Library Dependency bundling failed."); + try { + libraryProcessor.bundleLibraryDependencies(primaryLibrarySourcePath, fhirContext, resources, encoding, includeVersion); + } catch (Exception bre) { + failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Library Dependency bundling failed: " + bre.getMessage()); //exit from task: return null; } - shouldPersist = shouldPersist & result; } if (includePatientScenarios) { - boolean result = TestCaseProcessor.bundleTestCases(igPath, getResourceTestGroupName(), primaryLibraryName, fhirContext, resources); - if (shouldPersist && !result) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Test Case bundling failed."); + try { + TestCaseProcessor.bundleTestCases(igPath, getResourceTestGroupName(), primaryLibraryName, fhirContext, resources); + } catch (Exception tce) { + failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Test Case bundling failed: " + tce.getMessage()); //exit from task: return null; } - shouldPersist = shouldPersist & result; } if (shouldPersist) { @@ -263,17 +261,12 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } catch (Exception e) { String failMsg = ""; - if (e.getMessage() != null ){ + if (e.getMessage() != null) { failMsg = e.getMessage(); - }else{ - failMsg = e.getClass().getName(); - e.printStackTrace(); - } - if (resourceSourcePath == null || resourceSourcePath.isEmpty()) { - failedExceptionMessages.put(resourceEntry.getValue().getIdElement().getIdPart(), failMsg); } else { - failedExceptionMessages.put(resourceSourcePath, failMsg); + failMsg = e.getClass().getName(); } + failedExceptionMessages.put(resourceSourcePath, failMsg); } processedResources.add(resourceSourcePath); @@ -321,7 +314,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa List resourcePathLibraryNames = new ArrayList<>(getPaths(fhirContext)); //gather which resources didn't make it - ArrayList failedResources = new ArrayList<>(resourcePathLibraryNames); + List failedResources = new ArrayList<>(resourcePathLibraryNames); resourcePathLibraryNames.removeAll(bundledResources); resourcePathLibraryNames.retainAll(refreshedLibraryNames); @@ -335,7 +328,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa failedResources.removeAll(resourcePathLibraryNames); message.append(NEWLINE).append(failedResources.size()).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); for (String failed : failedResources) { - if (failedExceptionMessages.containsKey(failed)) { + if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); } else { message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 86dd128ac..045af7d96 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -280,9 +280,8 @@ public static String getId(String baseId) { return "tests-" + baseId; } - public static Boolean bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, - Map resources) { - Boolean shouldPersist = true; + public static void bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, + Map resources) throws Exception { String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT), contextResourceType), libraryName); // this is breaking for bundle of a bundle. Replace with individual resources @@ -294,18 +293,12 @@ public static Boolean bundleTestCases(String igPath, String contextResourceType, // resources, fhirContext); // } - try { - List testCaseResources = TestCaseProcessor.getTestCaseResources(igTestCasePath, fhirContext); - for (IBaseResource resource : testCaseResources) { - if ((!(resource instanceof org.hl7.fhir.dstu3.model.Bundle)) && (!(resource instanceof org.hl7.fhir.r4.model.Bundle))) { - resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); - } + List testCaseResources = TestCaseProcessor.getTestCaseResources(igTestCasePath, fhirContext); + for (IBaseResource resource : testCaseResources) { + if ((!(resource instanceof org.hl7.fhir.dstu3.model.Bundle)) && (!(resource instanceof org.hl7.fhir.r4.model.Bundle))) { + resources.putIfAbsent(resource.getIdElement().getIdPart(), resource); } - } catch (Exception e) { - shouldPersist = false; - logger.error(igTestCasePath, e); } - return shouldPersist; } From 62b356a7dbd66d88262bcecd36596905e552f753 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 18 Dec 2023 13:00:13 -0500 Subject: [PATCH 07/20] Safeguarding lists against concurrency issues. --- .../cqf/tooling/processor/AbstractBundler.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index c971f5994..d08d0ebbe 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -13,6 +13,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.Callable; @@ -132,10 +134,10 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa List> tasks = new ArrayList<>(); try { - final Map resourcesMap = getResources(fhirContext); - final Map libraryUrlMap = IOUtils.getLibraryUrlMap(fhirContext); - final Map libraries = IOUtils.getLibraries(fhirContext); - final Map libraryPathMap = IOUtils.getLibraryPathMap(fhirContext); + final Map resourcesMap = new ConcurrentHashMap<>(getResources(fhirContext)); + final Map libraryUrlMap = new ConcurrentHashMap<>(IOUtils.getLibraryUrlMap(fhirContext)); + final Map libraries = new ConcurrentHashMap<>(IOUtils.getLibraries(fhirContext)); + final Map libraryPathMap = new ConcurrentHashMap<>(IOUtils.getLibraryPathMap(fhirContext)); for (Map.Entry resourceEntry : resourcesMap.entrySet()) { String resourceId = ""; @@ -260,13 +262,16 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } catch (Exception e) { + PrintWriter pw = new PrintWriter(new StringWriter()); + e.printStackTrace(pw); + String failMsg = ""; if (e.getMessage() != null) { failMsg = e.getMessage(); } else { - failMsg = e.getClass().getName(); + failMsg = e.getClass().getName() ; } - failedExceptionMessages.put(resourceSourcePath, failMsg); + failedExceptionMessages.put(resourceSourcePath, failMsg + ":\r\n" + pw); } processedResources.add(resourceSourcePath); From 027878987cf39fd6e391e63bc4f354eb4948e1f3 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 18 Dec 2023 13:30:51 -0500 Subject: [PATCH 08/20] Adjusted some Maps in IOUtils to be concurrent safe, clarified POST summary during refresh. --- .../tooling/processor/AbstractBundler.java | 59 ++++++++++++------- .../cqf/tooling/utilities/IOUtils.java | 7 ++- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index d08d0ebbe..f3389619e 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -139,6 +139,11 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final Map libraries = new ConcurrentHashMap<>(IOUtils.getLibraries(fhirContext)); final Map libraryPathMap = new ConcurrentHashMap<>(IOUtils.getLibraryPathMap(fhirContext)); + if (resourcesMap.isEmpty()) { + System.out.println("[INFO] No " + getResourceProcessorType() + "s found. Continuing..."); + return; + } + for (Map.Entry resourceEntry : resourcesMap.entrySet()) { String resourceId = ""; @@ -271,7 +276,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } else { failMsg = e.getClass().getName() ; } - failedExceptionMessages.put(resourceSourcePath, failMsg + ":\r\n" + pw); + failedExceptionMessages.put(resourceSourcePath, failMsg + ":\r\n" + pw.toString()); } processedResources.add(resourceSourcePath); @@ -293,17 +298,16 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa StringBuilder message = new StringBuilder(NEWLINE); //Give user a snapshot of the files each resource will have persisted to their FHIR server (if fhirUri is provided) - if (!persistedFileReport.isEmpty()) { - message.append(NEWLINE).append(persistedFileReport.size()).append(" ").append(getResourceProcessorType()).append("(s) have POST tasks in the queue:"); + final int persistCount = persistedFileReport.size(); + if (persistCount > 0) { + message.append(NEWLINE).append(persistCount).append(" ").append(getResourceProcessorType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); int totalQueueCount = 0; for (String library : persistedFileReport.keySet()) { totalQueueCount = totalQueueCount + persistedFileReport.get(library); message.append(NEWLINE_INDENT) - .append(library) - .append(": ") .append(persistedFileReport.get(library)) - .append(" File(s) will be posted to ") - .append(fhirUri); + .append(" File(s): ") + .append(library); } message.append(NEWLINE_INDENT) .append("Total: ") @@ -311,37 +315,50 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa .append(" File(s)"); } - message.append(NEWLINE).append(bundledResources.size()).append(" ").append(getResourceProcessorType()).append("(s) successfully bundled:"); - for (String bundledResource : bundledResources) { - message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); + + final int bundledCount = bundledResources.size(); + if (bundledCount > 0) { + message.append(NEWLINE).append(bundledCount).append(" ").append(getResourceProcessorType()).append("(s) successfully bundled:"); + for (String bundledResource : bundledResources) { + message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); + } } + List resourcePathLibraryNames = new ArrayList<>(getPaths(fhirContext)); //gather which resources didn't make it List failedResources = new ArrayList<>(resourcePathLibraryNames); - resourcePathLibraryNames.removeAll(bundledResources); resourcePathLibraryNames.retainAll(refreshedLibraryNames); - message.append(NEWLINE).append(resourcePathLibraryNames.size()).append(" ").append(getResourceProcessorType()).append("(s) refreshed, but not bundled (due to issues):"); - for (String notBundled : resourcePathLibraryNames) { - message.append(NEWLINE_INDENT).append(notBundled).append(" REFRESHED"); + + final int refreshedNotBundledCount = resourcePathLibraryNames.size(); + if (refreshedNotBundledCount > 0) { + message.append(NEWLINE).append(refreshedNotBundledCount).append(" ").append(getResourceProcessorType()).append("(s) refreshed, but not bundled (due to issues):"); + for (String notBundled : resourcePathLibraryNames) { + message.append(NEWLINE_INDENT).append(notBundled).append(" REFRESHED"); + } } //attempt to give some kind of informational message: failedResources.removeAll(bundledResources); failedResources.removeAll(resourcePathLibraryNames); - message.append(NEWLINE).append(failedResources.size()).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); - for (String failed : failedResources) { - if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); - } else { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); + + final int failedCount = failedResources.size(); + if (failedCount > 0) { + message.append(NEWLINE).append(failedCount).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); + for (String failed : failedResources) { + if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { + message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); + } else { + message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); + } } } //Exceptions stemming from IOUtils.translate that did not prevent process from completing for file: - if (!cqlTranslatorErrorMessages.isEmpty()) { + final int translateErrorCount = cqlTranslatorErrorMessages.size(); + if (translateErrorCount > 0) { message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceProcessorType()).append("(s) encountered CQL translator errors:"); for (String library : cqlTranslatorErrorMessages.keySet()) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java index ed080682e..72344d0de 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java @@ -24,6 +24,7 @@ import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -197,7 +198,7 @@ public static void writeBundle(Object bundle, String path, Encoding encoding, Fh } - private static final Map alreadyCopied = new HashMap<>(); + private static final Map alreadyCopied = new ConcurrentHashMap<>(); public static void copyFile(String inputPath, String outputPath) { @@ -353,7 +354,7 @@ public static IBaseBundle bundleResourcesInDirectory(String directoryPath, FhirC public static boolean isDirectory(String path) { return FileUtils.isDirectory(new File(path)); } - private static final Map> cachedFilePaths = new HashMap<>(); + private static final Map> cachedFilePaths = new ConcurrentHashMap<>(); public static List getFilePaths(String directoryPath, Boolean recursive) { List filePaths = new ArrayList<>(); @@ -422,7 +423,7 @@ public static String getParentDirectoryPath(String path) { return file.getParent(); } - private static final Map> cachedDirectoryPaths = new HashMap<>(); + private static final Map> cachedDirectoryPaths = new ConcurrentHashMap<>(); public static List getDirectoryPaths(String path, Boolean recursive) { List directoryPaths = new ArrayList<>(); From 0825b156fd8394efffb5504077023400b5ea6c34 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 18 Dec 2023 13:41:40 -0500 Subject: [PATCH 09/20] Moved start/end messages for bundle process from igbundleprocessor to abstract class. Finished message won't appear if no resources exist for bundling. --- .../cqf/tooling/measure/MeasureBundler.java | 2 +- .../tooling/processor/AbstractBundler.java | 43 +++++++++++-------- .../tooling/processor/IGBundleProcessor.java | 16 ------- .../processor/PlanDefinitionBundler.java | 2 +- .../questionnaire/QuestionnaireBundler.java | 2 +- 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java index 10c2d95cb..7a91a3df4 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureBundler.java @@ -32,7 +32,7 @@ protected Map getResources(FhirContext fhirContext) { } @Override - protected String getResourceProcessorType() { + protected String getResourceBundlerType() { return TYPE_MEASURE; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index f3389619e..ba5c5f126 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -8,6 +8,7 @@ import org.opencds.cqf.tooling.common.ThreadUtils; import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.library.LibraryProcessor; +import org.opencds.cqf.tooling.measure.MeasureBundler; import org.opencds.cqf.tooling.utilities.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,12 +16,11 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.nio.file.Paths; +import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; /** * An abstract base class for bundlers that handle the bundling of various types of resources within an ig. @@ -82,7 +82,7 @@ protected List getIdentifiers() { } private String getResourcePrefix() { - return getResourceProcessorType().toLowerCase() + "-"; + return getResourceBundlerType().toLowerCase() + "-"; } protected abstract Set getPaths(FhirContext fhirContext); @@ -94,8 +94,11 @@ private String getResourcePrefix() { */ protected abstract Map getResources(FhirContext fhirContext); - protected abstract String getResourceProcessorType(); + protected abstract String getResourceBundlerType(); + private String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } /** * Bundles resources within an Implementation Guide based on specified options. @@ -115,6 +118,7 @@ private String getResourcePrefix() { public void bundleResources(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean includeVersion, Boolean addBundleTimestamp, FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean verboseMessaging) { + System.out.println("\r\n[Bundle " + getResourceBundlerType() + " has started - " + getTime() + "]\r\n"); final List bundledResources = new CopyOnWriteArrayList<>(); @@ -140,7 +144,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final Map libraryPathMap = new ConcurrentHashMap<>(IOUtils.getLibraryPathMap(fhirContext)); if (resourcesMap.isEmpty()) { - System.out.println("[INFO] No " + getResourceProcessorType() + "s found. Continuing..."); + System.out.println("[INFO] No " + getResourceBundlerType() + "s found. Continuing..."); return; } @@ -167,7 +171,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa tasks.add(() -> { //check if resourceSourcePath has been processed before: if (processedResources.contains(resourceSourcePath)) { - System.out.println(getResourceProcessorType() + " processed already: " + resourceSourcePath); + System.out.println(getResourceBundlerType() + " processed already: " + resourceSourcePath); return null; } String resourceName = FilenameUtils.getBaseName(resourceSourcePath).replace(getResourcePrefix(), ""); @@ -175,11 +179,11 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa try { Map resources = new ConcurrentHashMap<>(); Boolean shouldPersist = ResourceUtils.safeAddResource(resourceSourcePath, resources, fhirContext); - if (!resources.containsKey(getResourceProcessorType() + "/" + resourceEntry.getKey())) { - throw new IllegalArgumentException(String.format("Could not retrieve base resource for " + getResourceProcessorType() + " %s", resourceName)); + if (!resources.containsKey(getResourceBundlerType() + "/" + resourceEntry.getKey())) { + throw new IllegalArgumentException(String.format("Could not retrieve base resource for " + getResourceBundlerType() + " %s", resourceName)); } - IBaseResource resource = resources.get(getResourceProcessorType() + "/" + resourceEntry.getKey()); + IBaseResource resource = resources.get(getResourceBundlerType() + "/" + resourceEntry.getKey()); String primaryLibraryUrl = ResourceUtils.getPrimaryLibraryUrl(resource, fhirContext); IBaseResource primaryLibrary; if (primaryLibraryUrl != null && primaryLibraryUrl.startsWith("http")) { @@ -225,7 +229,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa try { libraryProcessor.bundleLibraryDependencies(primaryLibrarySourcePath, fhirContext, resources, encoding, includeVersion); } catch (Exception bre) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Library Dependency bundling failed: " + bre.getMessage()); + failedExceptionMessages.put(resourceSourcePath, getResourceBundlerType() + " will not be bundled because Library Dependency bundling failed: " + bre.getMessage()); //exit from task: return null; } @@ -235,7 +239,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa try { TestCaseProcessor.bundleTestCases(igPath, getResourceTestGroupName(), primaryLibraryName, fhirContext, resources); } catch (Exception tce) { - failedExceptionMessages.put(resourceSourcePath, getResourceProcessorType() + " will not be bundled because Test Case bundling failed: " + tce.getMessage()); + failedExceptionMessages.put(resourceSourcePath, getResourceBundlerType() + " will not be bundled because Test Case bundling failed: " + tce.getMessage()); //exit from task: return null; } @@ -291,7 +295,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa ThreadUtils.executeTasks(tasks); } catch (Exception e) { - LogUtils.putException("bundleResources: " + getResourceProcessorType(), e); + LogUtils.putException("bundleResources: " + getResourceBundlerType(), e); } //Prepare final report: @@ -300,7 +304,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa //Give user a snapshot of the files each resource will have persisted to their FHIR server (if fhirUri is provided) final int persistCount = persistedFileReport.size(); if (persistCount > 0) { - message.append(NEWLINE).append(persistCount).append(" ").append(getResourceProcessorType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); + message.append(NEWLINE).append(persistCount).append(" ").append(getResourceBundlerType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); int totalQueueCount = 0; for (String library : persistedFileReport.keySet()) { totalQueueCount = totalQueueCount + persistedFileReport.get(library); @@ -318,7 +322,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final int bundledCount = bundledResources.size(); if (bundledCount > 0) { - message.append(NEWLINE).append(bundledCount).append(" ").append(getResourceProcessorType()).append("(s) successfully bundled:"); + message.append(NEWLINE).append(bundledCount).append(" ").append(getResourceBundlerType()).append("(s) successfully bundled:"); for (String bundledResource : bundledResources) { message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); } @@ -334,7 +338,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final int refreshedNotBundledCount = resourcePathLibraryNames.size(); if (refreshedNotBundledCount > 0) { - message.append(NEWLINE).append(refreshedNotBundledCount).append(" ").append(getResourceProcessorType()).append("(s) refreshed, but not bundled (due to issues):"); + message.append(NEWLINE).append(refreshedNotBundledCount).append(" ").append(getResourceBundlerType()).append("(s) refreshed, but not bundled (due to issues):"); for (String notBundled : resourcePathLibraryNames) { message.append(NEWLINE_INDENT).append(notBundled).append(" REFRESHED"); } @@ -346,7 +350,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final int failedCount = failedResources.size(); if (failedCount > 0) { - message.append(NEWLINE).append(failedCount).append(" ").append(getResourceProcessorType()).append("(s) failed refresh:"); + message.append(NEWLINE).append(failedCount).append(" ").append(getResourceBundlerType()).append("(s) failed refresh:"); for (String failed : failedResources) { if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); @@ -359,7 +363,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa //Exceptions stemming from IOUtils.translate that did not prevent process from completing for file: final int translateErrorCount = cqlTranslatorErrorMessages.size(); if (translateErrorCount > 0) { - message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceProcessorType()).append("(s) encountered CQL translator errors:"); + message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceBundlerType()).append("(s) encountered CQL translator errors:"); for (String library : cqlTranslatorErrorMessages.keySet()) { message.append(NEWLINE_INDENT).append( @@ -369,16 +373,17 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } System.out.println(message); + System.out.println("\r\n[Bundle " + getResourceBundlerType() + " has finished - " + getTime() + "]\r\n"); } private void reportProgress(int count, int total) { double percentage = (double) count / total * 100; - System.out.print("\rBundle " + getResourceProcessorType() + "s: " + String.format("%.2f%%", percentage) + " processed."); + System.out.print("\rBundle " + getResourceBundlerType() + "s: " + String.format("%.2f%%", percentage) + " processed."); } private String getResourceTestGroupName() { - return getResourceProcessorType().toLowerCase(); + return getResourceBundlerType().toLowerCase(); } private void persistBundle(String bundleDestPath, String libraryName, diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java index 4dd80b96d..085e4c579 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java @@ -34,34 +34,18 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean versioned, Boolean addBundleTimestamp, FhirContext fhirContext, String fhirUri) { - System.out.println("\n"); - - System.out.println("\r\n[Bundle Measures has started - " + getTime() + "]\r\n"); new MeasureBundler().bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, fhirUri, encoding, verboseMessaging); - - System.out.println("\r\n[Bundle Measures has finished - " + getTime() + "]\r\n"); - - - System.out.println("\r\n[Bundle PlanDefinitions has started - " + getTime() + "]\r\n"); new PlanDefinitionBundler(this.libraryProcessor, this.cdsHooksProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, fhirUri, encoding, verboseMessaging); - System.out.println("\r\n[Bundle PlanDefinitions has finished - " + getTime() + "]\r\n"); - - - - System.out.println("\r\n[Bundle Questionnaires has started - " + getTime() + "]\r\n"); new QuestionnaireBundler(this.libraryProcessor).bundleResources(refreshedLibraryNames, igPath, binaryPaths, includeDependencies, includeTerminology, includePatientScenarios, versioned, addBundleTimestamp, fhirContext, fhirUri, encoding, verboseMessaging); - System.out.println("\r\n[Bundle Questionnaires has finished - " + getTime() + "]\r\n"); - - //run collected post calls last: if (HttpClientUtils.hasPostTasksInQueue()) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java index 7e0df5204..8f4690bee 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/PlanDefinitionBundler.java @@ -31,7 +31,7 @@ protected Map getResources(FhirContext fhirContext) { } @Override - protected String getResourceProcessorType() { + protected String getResourceBundlerType() { return TYPE_PLAN_DEFINITION; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java index 6dd85c315..d605c9694 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/questionnaire/QuestionnaireBundler.java @@ -31,7 +31,7 @@ protected Map getResources(FhirContext fhirContext) { } @Override - protected String getResourceProcessorType() { + protected String getResourceBundlerType() { return TYPE_QUESTIONNAIRE; } From ac434280d9510f665ef30fe243c7cbf1b6054457 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 18 Dec 2023 13:50:48 -0500 Subject: [PATCH 10/20] Consistent titling of sections in console (ie [Bundling Measures] --- .../org/opencds/cqf/tooling/processor/AbstractBundler.java | 3 +-- .../org/opencds/cqf/tooling/processor/IGBundleProcessor.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index ba5c5f126..79722142d 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -118,7 +118,7 @@ private String getTime() { public void bundleResources(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean includeVersion, Boolean addBundleTimestamp, FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean verboseMessaging) { - System.out.println("\r\n[Bundle " + getResourceBundlerType() + " has started - " + getTime() + "]\r\n"); + System.out.println("\r\n[Bundling " + getResourceBundlerType() + "s]\r\n"); final List bundledResources = new CopyOnWriteArrayList<>(); @@ -373,7 +373,6 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } System.out.println(message); - System.out.println("\r\n[Bundle " + getResourceBundlerType() + " has finished - " + getTime() + "]\r\n"); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java index 085e4c579..8a2c9b20b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java @@ -49,9 +49,8 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis //run collected post calls last: if (HttpClientUtils.hasPostTasksInQueue()) { - System.out.println("\r\n[POST task(s) found in queue. POST task(s) started - " + getTime() + "]"); + System.out.println("\r\n[Persisting Files to " + fhirUri + "]\r\n"); HttpClientUtils.postTaskCollection(); - System.out.println("\r\n[POST task(s) finished - " + getTime() + "]"); } // run cleanup (maven runs all ci tests sequentially and static member variables could retain values from previous tests) From d9570c8e7ec338302c31603692139e2ca96542fd Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 18 Dec 2023 19:13:52 -0500 Subject: [PATCH 11/20] Group file creation bug resolved. --- .../tooling/processor/AbstractBundler.java | 8 +- .../tooling/processor/TestCaseProcessor.java | 223 +++++++++--------- 2 files changed, 111 insertions(+), 120 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index 79722142d..0ce0ab9f3 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.cqframework.cql.cql2elm.CqlCompilerException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -271,16 +272,13 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } catch (Exception e) { - PrintWriter pw = new PrintWriter(new StringWriter()); - e.printStackTrace(pw); - String failMsg = ""; if (e.getMessage() != null) { failMsg = e.getMessage(); } else { - failMsg = e.getClass().getName() ; + failMsg = e.getClass().getName() + ":\r\n" + ExceptionUtils.getStackTrace(e); } - failedExceptionMessages.put(resourceSourcePath, failMsg + ":\r\n" + pw.toString()); + failedExceptionMessages.put(resourceSourcePath, failMsg); } processedResources.add(resourceSourcePath); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 045af7d96..a751b6d3f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.*; @@ -38,137 +39,117 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext System.out.println("\r\n[Refreshing Tests]\r\n"); - final Map testCaseRefreshSuccessMap = new ConcurrentHashMap<>(); - final Map testCaseRefreshFailMap = new ConcurrentHashMap<>(); - final Map groupFileRefreshSuccessMap = new ConcurrentHashMap<>(); - final Map groupFileRefreshFailMap = new ConcurrentHashMap<>(); + final Map testCaseRefreshSuccessMap = new HashMap<>(); + final Map testCaseRefreshFailMap = new HashMap<>(); + final Map groupFileRefreshSuccessMap = new HashMap<>(); + final Map groupFileRefreshFailMap = new HashMap<>(); + - final List> testCaseRefreshTasks = new CopyOnWriteArrayList<>(); IFhirVersion version = fhirContext.getVersion(); + + final int totalTestFileCount = getTestFileCount(path); + //build list of tasks via for loop: - List> testGroupTasks = new ArrayList<>(); List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); + for (String group : resourceTypeTestGroups) { - testGroupTasks.add(() -> { - List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); - - //build list of tasks via for loop: - List> testArtifactTasks = new CopyOnWriteArrayList<>(); - ExecutorService testArtifactExecutor = Executors.newCachedThreadPool(); - - for (String testArtifactPath : testArtifactPaths) { - testArtifactTasks.add(() -> { - List testCasePaths = IOUtils.getDirectoryPaths(testArtifactPath, false); - - org.hl7.fhir.r4.model.Group testGroup; - if (version.getVersion() == FhirVersionEnum.R4) { - testGroup = new org.hl7.fhir.r4.model.Group(); - testGroup.setActive(true); - testGroup.setType(Group.GroupType.PERSON); - testGroup.setActual(true); - } else { - testGroup = null; - } - // For each test case we need to create a group - if (!testCasePaths.isEmpty()) { - String measureName = IOUtils.getMeasureTestDirectory(testCasePaths.get(0)); + List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); - if (testGroup != null) { - testGroup.setId(measureName); + for (String testArtifactPath : testArtifactPaths) { + List testCasePaths = IOUtils.getDirectoryPaths(testArtifactPath, false); - testGroup.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-testArtifact", - new CanonicalType("http://ecqi.healthit.gov/ecqms/Measure/" + measureName)); - } + org.hl7.fhir.r4.model.Group testGroup; + if (version.getVersion() == FhirVersionEnum.R4) { + testGroup = new org.hl7.fhir.r4.model.Group(); + testGroup.setActive(true); + testGroup.setType(Group.GroupType.PERSON); + testGroup.setActual(true); + } else { + testGroup = null; + } - for (String testCasePath : testCasePaths) { - testCaseRefreshTasks.add(() -> { - try { - List paths = IOUtils.getFilePaths(testCasePath, true); - List resources = IOUtils.readResources(paths, fhirContext); - ensureIds(testCasePath, resources); - - // Loop through resources and any that are patients need to be added to the test Group - // Handle individual resources when they exist - for (IBaseResource resource : resources) { - if ((resource.fhirType().equalsIgnoreCase("Patient")) && (version.getVersion() == FhirVersionEnum.R4)) { - org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) resource; - if (testGroup != null) { - addPatientToGroupR4(testGroup, patient); - } - } + // For each test case we need to create a group + if (!testCasePaths.isEmpty()) { + String measureName = IOUtils.getMeasureTestDirectory(testCasePaths.get(0)); + + if (testGroup != null) { + testGroup.setId(measureName); + + testGroup.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-testArtifact", + new CanonicalType("http://ecqi.healthit.gov/ecqms/Measure/" + measureName)); + } + + for (String testCasePath : testCasePaths) { + try { + List paths = IOUtils.getFilePaths(testCasePath, true); + List resources = IOUtils.readResources(paths, fhirContext); + ensureIds(testCasePath, resources); + + // Loop through resources and any that are patients need to be added to the test Group + // Handle individual resources when they exist + for (IBaseResource resource : resources) { + if ((resource.fhirType().equalsIgnoreCase("Patient")) && (version.getVersion() == FhirVersionEnum.R4)) { + org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) resource; + if (testGroup != null) { + addPatientToGroupR4(testGroup, patient); + } + } - // Handle bundled resources when that is how they are provided - if ((resource.fhirType().equalsIgnoreCase("Bundle")) && (version.getVersion() == FhirVersionEnum.R4)) { - org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) resource; - var bundleResources = - BundleUtils.getR4ResourcesFromBundle(bundle); - for (IBaseResource bundleResource : bundleResources) { - if (bundleResource.fhirType().equalsIgnoreCase("Patient")) { - org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) bundleResource; - if (testGroup != null) { - addPatientToGroupR4(testGroup, patient); - } - } - } + // Handle bundled resources when that is how they are provided + if ((resource.fhirType().equalsIgnoreCase("Bundle")) && (version.getVersion() == FhirVersionEnum.R4)) { + org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) resource; + var bundleResources = + BundleUtils.getR4ResourcesFromBundle(bundle); + for (IBaseResource bundleResource : bundleResources) { + if (bundleResource.fhirType().equalsIgnoreCase("Patient")) { + org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) bundleResource; + if (testGroup != null) { + addPatientToGroupR4(testGroup, patient); } } - - // If the resource is a transaction bundle then don't bundle it again otherwise do - String fileId = getId(FilenameUtils.getName(testCasePath)); - Object bundle; - if ((resources.size() == 1) && (BundleUtils.resourceIsABundle(resources.get(0)))) { - bundle = processTestBundle(fileId, resources.get(0), fhirContext, testArtifactPath, testCasePath); - } else { - bundle = BundleUtils.bundleArtifacts(fileId, resources, fhirContext, false); - } - IOUtils.writeBundle(bundle, testArtifactPath, encoding, fhirContext); - - } catch (Exception e) { - testCaseRefreshFailMap.put(testCasePath, e.getMessage()); } + } + } + // If the resource is a transaction bundle then don't bundle it again otherwise do + String fileId = getId(FilenameUtils.getName(testCasePath)); + Object bundle; + if ((resources.size() == 1) && (BundleUtils.resourceIsABundle(resources.get(0)))) { + bundle = processTestBundle(fileId, resources.get(0), fhirContext, testArtifactPath, testCasePath); + } else { + bundle = BundleUtils.bundleArtifacts(fileId, resources, fhirContext, false); + } + IOUtils.writeBundle(bundle, testArtifactPath, encoding, fhirContext); - testCaseRefreshSuccessMap.put(testCasePath, ""); - reportProgress((testCaseRefreshFailMap.size() + testCaseRefreshSuccessMap.size()), testCaseRefreshTasks.size()); - //task requires return statement - return null; - }); - }//end for (String testCasePath : testCasePaths) { - - // Need to output the Group if it exists - if (testGroup != null) { - String groupFileName = "Group-" + measureName; - String groupFileIdentifier = testArtifactPath + separator + groupFileName; + } catch (Exception e) { + testCaseRefreshFailMap.put(testCasePath, e.getMessage()); + } + testCaseRefreshSuccessMap.put(testCasePath, ""); + reportProgress((testCaseRefreshSuccessMap.size() + testCaseRefreshFailMap.size()), totalTestFileCount); + } - try { - IOUtils.writeResource(testGroup, testArtifactPath, encoding, fhirContext, true, - groupFileName); + // Need to output the Group if it exists + if (testGroup != null) { + String groupFileName = "Group-" + measureName; + String groupFileIdentifier = testArtifactPath + separator + groupFileName; - groupFileRefreshSuccessMap.put(groupFileIdentifier, ""); + try { + IOUtils.writeResource(testGroup, testArtifactPath, encoding, fhirContext, true, + groupFileName); - } catch (Exception e) { + groupFileRefreshSuccessMap.put(groupFileIdentifier, ""); - groupFileRefreshFailMap.put(groupFileIdentifier, e.getMessage()); - } + } catch (Exception e) { - } + groupFileRefreshFailMap.put(groupFileIdentifier, e.getMessage()); } - //task requires return statement - return null; - }); - }// - ThreadUtils.executeTasks(testArtifactTasks, testArtifactExecutor); - - //task requires return statement - return null; - }); - }//end for (String group : resourceTypeTestGroups) { - ThreadUtils.executeTasks(testGroupTasks); - //Now with all possible tasks collected, progress can be reported instead of flooding the console. - ThreadUtils.executeTasks(testCaseRefreshTasks); - //ensure accurate progress at final stage: - reportProgress((testCaseRefreshFailMap.size() + testCaseRefreshSuccessMap.size()), testCaseRefreshTasks.size()); + + } + } + } + + } StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", verboseMessaging); if (!groupFileRefreshSuccessMap.isEmpty() || !groupFileRefreshFailMap.isEmpty()) { @@ -177,13 +158,25 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext System.out.println(testCaseMessage); } + private int getTestFileCount(String path) { + int counter = 0; + List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); + for (String group : resourceTypeTestGroups) { + List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); + for (String testArtifactPath : testArtifactPaths) { + counter += IOUtils.getDirectoryPaths(testArtifactPath, false).size(); + } + } + return counter; + } + /** * Gives the user a nice report at the end of the refresh test case process (used to report group file status as well) * - * @param failMap which items failed - * @param successMap which items succeeded - * @param type group file or test case - * @param successType created or refreshed + * @param failMap which items failed + * @param successMap which items succeeded + * @param type group file or test case + * @param successType created or refreshed * @param verboseMessaging give the exception message if verboseMessaging is on * @return built message for console */ @@ -281,7 +274,7 @@ public static String getId(String baseId) { } public static void bundleTestCases(String igPath, String contextResourceType, String libraryName, FhirContext fhirContext, - Map resources) throws Exception { + Map resources) throws Exception { String igTestCasePath = FilenameUtils.concat(FilenameUtils.concat(FilenameUtils.concat(igPath, IGProcessor.TEST_CASE_PATH_ELEMENT), contextResourceType), libraryName); // this is breaking for bundle of a bundle. Replace with individual resources From f352f9ad1562bb912a273bd91288ffff6adb153f Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Tue, 19 Dec 2023 09:37:27 -0500 Subject: [PATCH 12/20] Added Group file verification during Refresh IG test. --- .../operation/RefreshIGOperationTest.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java index 7c26478d6..8a2a795ca 100644 --- a/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java +++ b/tooling/src/test/java/org/opencds/cqf/tooling/operation/RefreshIGOperationTest.java @@ -1,8 +1,5 @@ package org.opencds.cqf.tooling.operation; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; - import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -19,6 +16,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; @@ -46,6 +44,10 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; + +import static org.testng.Assert.*; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.Reference; public class RefreshIGOperationTest extends RefreshTest { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); public RefreshIGOperationTest() { @@ -163,11 +165,28 @@ public void testBundledFiles() throws IOException { // loop through each file, determine resourceType and treat accordingly Map resourceTypeMap = new HashMap<>(); + List groupPatientList = new ArrayList<>(); try (final DirectoryStream dirStream = Files.newDirectoryStream(dir)) { dirStream.forEach(path -> { File file = new File(path.toString()); + //Group file testing: + if (file.getName().equalsIgnoreCase("Group-BreastCancerScreeningFHIR.json")){ + try{ + org.hl7.fhir.r4.model.Group group = (org.hl7.fhir.r4.model.Group)IOUtils.readResource(file.getAbsolutePath(), fhirContext); + assertTrue(group.hasMember()); + // Check if the group contains members + // Iterate through the members + for (Group.GroupMemberComponent member : group.getMember()) { + groupPatientList.add(member.getEntity().getDisplay()); + } + }catch (Exception e){ + fail("Group-BreastCancerScreeningFHIR.json did not parse to valid Group instance."); + } + + } + if (file.getName().toLowerCase().endsWith(".json")) { Map map = this.jsonMap(file); @@ -204,6 +223,9 @@ public void testBundledFiles() throws IOException { logger.info(e.getMessage()); } + //Group file should contain two patients: + assertEquals(groupPatientList.size(), 2); + // map out entries in the resulting single bundle file: Map bundledJson = this.jsonMap(new File(bundledFileResult)); Map bundledJsonResourceTypes = new HashMap<>(); From e22f9caa777b8b3b59c6b1325fff131e6f8994e0 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 21 Dec 2023 15:51:13 -0500 Subject: [PATCH 13/20] Added failed test tracking so we don't have to delete any test files and can cleanly report to measure developers the issues they have with tests. --- .../tooling/processor/TestCaseProcessor.java | 101 +++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index a751b6d3f..537ce0ef8 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -17,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -25,10 +26,30 @@ public class TestCaseProcessor { public static final String NEWLINE_INDENT = "\r\n\t"; + public static final String NEWLINE_INDENT2 = "\r\n\t\t"; public static final String NEWLINE = "\r\n"; public static final String separator = System.getProperty("file.separator"); - private static final Logger logger = LoggerFactory.getLogger(TestCaseProcessor.class); + + private Map getIgnoredTestList() { + Map ignoredTestList = new HashMap<>(); + File ignoreTestsFile = new File("ignore_tests.txt"); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(ignoreTestsFile))) { + String entry; + while ((entry = bufferedReader.readLine()) != null) { + if (entry.contains(":")) { + String testCase = entry.split(":")[0]; + ignoredTestList.put(testCase, + entry.replace(testCase, "")); + } else { + ignoredTestList.put(entry, ""); + } + } + } catch (Exception e) { + //no file exists, that's ok. Return a blank Map. + } + return ignoredTestList; + } public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, Boolean verboseMessaging) { refreshTestCases(path, encoding, fhirContext, null, verboseMessaging); @@ -38,13 +59,12 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext Boolean verboseMessaging) { System.out.println("\r\n[Refreshing Tests]\r\n"); - final Map testCaseRefreshSuccessMap = new HashMap<>(); final Map testCaseRefreshFailMap = new HashMap<>(); + final Map groupFileRefreshSuccessMap = new HashMap<>(); final Map groupFileRefreshFailMap = new HashMap<>(); - IFhirVersion version = fhirContext.getVersion(); final int totalTestFileCount = getTestFileCount(path); @@ -52,11 +72,17 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext //build list of tasks via for loop: List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); + Map ignoredTestList = getIgnoredTestList(); + final Map> testCaseRefreshIgnoredMap = new HashMap<>(); + for (String group : resourceTypeTestGroups) { List testArtifactPaths = IOUtils.getDirectoryPaths(group, false); for (String testArtifactPath : testArtifactPaths) { + + String artifact = FilenameUtils.getName(testArtifactPath); + List testCasePaths = IOUtils.getDirectoryPaths(testArtifactPath, false); org.hl7.fhir.r4.model.Group testGroup; @@ -81,6 +107,35 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext } for (String testCasePath : testCasePaths) { + String testCase = FilenameUtils.getName(testCasePath); + String fileId = getId(testCase); + + if (ignoredTestList.containsKey(testCase)) { + Map artifactTestsIgnoredMap; + //try to group the tests by artifact: + if (testCaseRefreshIgnoredMap.containsKey(artifact)){ + artifactTestsIgnoredMap = new HashMap<>(testCaseRefreshIgnoredMap.get(artifact)); + }else{ + artifactTestsIgnoredMap = new HashMap<>(); + } + //add the test case and reason specified in file: + artifactTestsIgnoredMap.put(testCase, ignoredTestList.get(testCase)); + + //add this map to collection of maps: + testCaseRefreshIgnoredMap.put(artifact, artifactTestsIgnoredMap); + + //try to delete any existing tests-* files that may have been created in previous tests: + File testCaseDeleteFile = new File(testArtifactPath + separator + fileId + "-bundle.json"); + if (testCaseDeleteFile.exists()) { + try { + testCaseDeleteFile.delete(); + }catch (Exception e){ + //something went wrong in deleting the old test file, it won't interfere with group file creation though + } + } + continue; + } + try { List paths = IOUtils.getFilePaths(testCasePath, true); List resources = IOUtils.readResources(paths, fhirContext); @@ -113,7 +168,6 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext } // If the resource is a transaction bundle then don't bundle it again otherwise do - String fileId = getId(FilenameUtils.getName(testCasePath)); Object bundle; if ((resources.size() == 1) && (BundleUtils.resourceIsABundle(resources.get(0)))) { bundle = processTestBundle(fileId, resources.get(0), fhirContext, testArtifactPath, testCasePath); @@ -141,20 +195,41 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext groupFileRefreshSuccessMap.put(groupFileIdentifier, ""); } catch (Exception e) { - groupFileRefreshFailMap.put(groupFileIdentifier, e.getMessage()); } - } } } - } StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", verboseMessaging); + if (!groupFileRefreshSuccessMap.isEmpty() || !groupFileRefreshFailMap.isEmpty()) { testCaseMessage.append(buildInformationMessage(groupFileRefreshFailMap, groupFileRefreshSuccessMap, "Group File", "Created", verboseMessaging)); } + + //There were specified ignored tests, categorize by artifact: + if (!testCaseRefreshIgnoredMap.isEmpty()) { + int totalTestCaseIgnoredCount = 0; + for (Map testCaseMap : testCaseRefreshIgnoredMap.values()) { + totalTestCaseIgnoredCount += testCaseMap.size(); + } + testCaseMessage.append(NEWLINE).append(totalTestCaseIgnoredCount).append(" Test Case(s) were designated to be ignored:"); + for (String artifact : testCaseRefreshIgnoredMap.keySet()) { + testCaseMessage.append(NEWLINE_INDENT) + .append(artifact) + .append(": "); + + Map testCaseIgnored = testCaseRefreshIgnoredMap.get(artifact); + + for (Map.Entry entry : testCaseIgnored.entrySet()){ + testCaseMessage.append(NEWLINE_INDENT2) + .append(entry.getKey()) + //get the reason specified by the ignore file: + .append(entry.getValue()); + } + } + } System.out.println(testCaseMessage); } @@ -182,16 +257,16 @@ private int getTestFileCount(String path) { */ private StringBuilder buildInformationMessage(Map failMap, Map successMap, String type, String successType, boolean verboseMessaging) { StringBuilder message = new StringBuilder(); - if (!successMap.isEmpty() || !failMap.isEmpty()) { + if (!successMap.isEmpty()) { message.append(NEWLINE).append(successMap.size()).append(" ").append(type).append("(s) successfully ").append(successType.toLowerCase()).append(":"); for (String refreshedTestCase : successMap.keySet()) { message.append(NEWLINE_INDENT).append(refreshedTestCase).append(" ").append(successType.toUpperCase()); } - if (!failMap.isEmpty()) { - message.append(NEWLINE).append(failMap.size()).append(" ").append(type).append("(s) failed to be ").append(successType.toLowerCase()).append(":"); - for (String failed : failMap.keySet()) { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED").append(verboseMessaging ? ": " + failMap.get(failed) : ""); - } + } + if (!failMap.isEmpty()) { + message.append(NEWLINE).append(failMap.size()).append(" ").append(type).append("(s) failed to be ").append(successType.toLowerCase()).append(":"); + for (String failed : failMap.keySet()) { + message.append(NEWLINE_INDENT).append(failed).append(" FAILED").append(verboseMessaging ? ": " + failMap.get(failed) : ""); } } return message; From 5a44f38c1116dd9e4068dd74d4a5cc4d4f6b75d1 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 21 Dec 2023 16:02:49 -0500 Subject: [PATCH 14/20] Moving some hard coded strings to constants --- .../tooling/processor/TestCaseProcessor.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 537ce0ef8..0ecb21f7b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -10,19 +10,15 @@ import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Reference; -import org.opencds.cqf.tooling.common.ThreadUtils; import org.opencds.cqf.tooling.utilities.BundleUtils; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.ResourceUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.*; public class TestCaseProcessor { public static final String NEWLINE_INDENT = "\r\n\t"; @@ -30,6 +26,11 @@ public class TestCaseProcessor { public static final String NEWLINE = "\r\n"; public static final String separator = System.getProperty("file.separator"); + public static final String TEST_ARTIFACT_URL = "http://hl7.org/fhir/StructureDefinition/artifact-testArtifact"; + public static final String MEASURE_URL = "http://ecqi.healthit.gov/ecqms/Measure/"; + public static final String PATIENT_TYPE = "Patient"; + public static final String BUNDLE_TYPE = "Bundle"; + public static final String GROUP_FILE_SEPARATOR = "Group-"; private Map getIgnoredTestList() { Map ignoredTestList = new HashMap<>(); @@ -102,8 +103,8 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext if (testGroup != null) { testGroup.setId(measureName); - testGroup.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-testArtifact", - new CanonicalType("http://ecqi.healthit.gov/ecqms/Measure/" + measureName)); + testGroup.addExtension(TEST_ARTIFACT_URL, + new CanonicalType(MEASURE_URL + measureName)); } for (String testCasePath : testCasePaths) { @@ -144,7 +145,7 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext // Loop through resources and any that are patients need to be added to the test Group // Handle individual resources when they exist for (IBaseResource resource : resources) { - if ((resource.fhirType().equalsIgnoreCase("Patient")) && (version.getVersion() == FhirVersionEnum.R4)) { + if ((resource.fhirType().equalsIgnoreCase(PATIENT_TYPE)) && (version.getVersion() == FhirVersionEnum.R4)) { org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) resource; if (testGroup != null) { addPatientToGroupR4(testGroup, patient); @@ -152,12 +153,12 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext } // Handle bundled resources when that is how they are provided - if ((resource.fhirType().equalsIgnoreCase("Bundle")) && (version.getVersion() == FhirVersionEnum.R4)) { + if ((resource.fhirType().equalsIgnoreCase(BUNDLE_TYPE)) && (version.getVersion() == FhirVersionEnum.R4)) { org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) resource; var bundleResources = BundleUtils.getR4ResourcesFromBundle(bundle); for (IBaseResource bundleResource : bundleResources) { - if (bundleResource.fhirType().equalsIgnoreCase("Patient")) { + if (bundleResource.fhirType().equalsIgnoreCase(PATIENT_TYPE)) { org.hl7.fhir.r4.model.Patient patient = (org.hl7.fhir.r4.model.Patient) bundleResource; if (testGroup != null) { addPatientToGroupR4(testGroup, patient); @@ -185,7 +186,7 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext // Need to output the Group if it exists if (testGroup != null) { - String groupFileName = "Group-" + measureName; + String groupFileName = GROUP_FILE_SEPARATOR + measureName; String groupFileIdentifier = testArtifactPath + separator + groupFileName; try { From b7ef5f96df2ff330775485dcb5fa85ddf0804eaf Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Fri, 22 Dec 2023 09:07:29 -0500 Subject: [PATCH 15/20] Added failed POST logging so users can retain a log of what files fail post. Added ability to ignore certain tests during refresh. --- .../cqf/tooling/common/ThreadUtils.java | 12 ++- .../tooling/processor/TestCaseProcessor.java | 98 +++++++++++-------- .../tooling/utilities/HttpClientUtils.java | 25 +++-- 3 files changed, 84 insertions(+), 51 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java index a2cade717..72f78e7d1 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/common/ThreadUtils.java @@ -70,10 +70,14 @@ public static void executeTasks(Queue> callables) { } public static void shutdownRunningExecutors() { - if (runningExecutors.isEmpty()) return; - for (ExecutorService es : runningExecutors){ - es.shutdownNow(); + try { + if (runningExecutors.isEmpty()) return; + for (ExecutorService es : runningExecutors) { + es.shutdownNow(); + } + runningExecutors = new ArrayList<>(); + }catch (Exception e){ + //fail silently, shutting down anyways } - runningExecutors = new ArrayList<>(); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 0ecb21f7b..8adf44d18 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -19,6 +19,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public class TestCaseProcessor { public static final String NEWLINE_INDENT = "\r\n\t"; @@ -35,19 +38,23 @@ public class TestCaseProcessor { private Map getIgnoredTestList() { Map ignoredTestList = new HashMap<>(); File ignoreTestsFile = new File("ignore_tests.txt"); - try (BufferedReader bufferedReader = new BufferedReader(new FileReader(ignoreTestsFile))) { - String entry; - while ((entry = bufferedReader.readLine()) != null) { - if (entry.contains(":")) { - String testCase = entry.split(":")[0]; - ignoredTestList.put(testCase, - entry.replace(testCase, "")); - } else { - ignoredTestList.put(entry, ""); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(new File("ignore_tests.txt")))) { + for (String input : bufferedReader.lines().collect(Collectors.toList())) { + try { + int startIndex = input.indexOf("tests-") + "tests-".length(); + int endIndex = input.indexOf("-bundle.json"); + String uuid = input.substring(startIndex, endIndex); + + int hapiIndex = input.indexOf("HAPI-"); + String hapiMessage = input.substring(hapiIndex); + + ignoredTestList.put(uuid, hapiMessage); + } catch (Exception e) { + //parsing failed, that's ok, continue through the loop } } } catch (Exception e) { - //no file exists, that's ok. Return a blank Map. + //no file exists, that's ok. ignoredTestList is simply blank and ignored later. } return ignoredTestList; } @@ -73,7 +80,7 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext //build list of tasks via for loop: List resourceTypeTestGroups = IOUtils.getDirectoryPaths(path, false); - Map ignoredTestList = getIgnoredTestList(); + Map ignoredTestsList = getIgnoredTestList(); final Map> testCaseRefreshIgnoredMap = new HashMap<>(); for (String group : resourceTypeTestGroups) { @@ -108,37 +115,46 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext } for (String testCasePath : testCasePaths) { - String testCase = FilenameUtils.getName(testCasePath); - String fileId = getId(testCase); - - if (ignoredTestList.containsKey(testCase)) { - Map artifactTestsIgnoredMap; - //try to group the tests by artifact: - if (testCaseRefreshIgnoredMap.containsKey(artifact)){ - artifactTestsIgnoredMap = new HashMap<>(testCaseRefreshIgnoredMap.get(artifact)); - }else{ - artifactTestsIgnoredMap = new HashMap<>(); - } - //add the test case and reason specified in file: - artifactTestsIgnoredMap.put(testCase, ignoredTestList.get(testCase)); - - //add this map to collection of maps: - testCaseRefreshIgnoredMap.put(artifact, artifactTestsIgnoredMap); - - //try to delete any existing tests-* files that may have been created in previous tests: - File testCaseDeleteFile = new File(testArtifactPath + separator + fileId + "-bundle.json"); - if (testCaseDeleteFile.exists()) { - try { - testCaseDeleteFile.delete(); - }catch (Exception e){ - //something went wrong in deleting the old test file, it won't interfere with group file creation though - } - } - continue; - } + reportProgress((testCaseRefreshSuccessMap.size() + testCaseRefreshFailMap.size()), totalTestFileCount); try { List paths = IOUtils.getFilePaths(testCasePath, true); + String testCase = FilenameUtils.getName(testCasePath); + String fileId = getId(testCase); + + //ignore the test if specified in ignore_tests.txt in root dir: + if (ignoredTestsList.containsKey(testCase)) { + Map artifactTestsIgnoredMap; + //try to group the tests by artifact: + if (testCaseRefreshIgnoredMap.containsKey(artifact)) { + artifactTestsIgnoredMap = new HashMap<>(testCaseRefreshIgnoredMap.get(artifact)); + } else { + artifactTestsIgnoredMap = new HashMap<>(); + } + //add the test case and reason specified in file: + StringBuilder pathsString = new StringBuilder(); +// //attach actual filename after uuid for easier identification: +// for (String pathStr : paths){ +// pathsString.append("(") +// .append(FilenameUtils.getName(pathStr)) +// .append(")"); +// } + artifactTestsIgnoredMap.put(testCase + pathsString, ignoredTestsList.get(testCase)); + + //add this map to collection of maps: + testCaseRefreshIgnoredMap.put(artifact, artifactTestsIgnoredMap); + + //try to delete any existing tests-* files that may have been created in previous tests: + File testCaseDeleteFile = new File(testArtifactPath + separator + fileId + "-bundle.json"); + if (testCaseDeleteFile.exists()) { + try { + testCaseDeleteFile.delete(); + } catch (Exception e) { + //something went wrong in deleting the old test file, it won't interfere with group file creation though + } + } + continue; + } List resources = IOUtils.readResources(paths, fhirContext); ensureIds(testCasePath, resources); @@ -181,7 +197,6 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext testCaseRefreshFailMap.put(testCasePath, e.getMessage()); } testCaseRefreshSuccessMap.put(testCasePath, ""); - reportProgress((testCaseRefreshSuccessMap.size() + testCaseRefreshFailMap.size()), totalTestFileCount); } // Need to output the Group if it exists @@ -223,9 +238,10 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext Map testCaseIgnored = testCaseRefreshIgnoredMap.get(artifact); - for (Map.Entry entry : testCaseIgnored.entrySet()){ + for (Map.Entry entry : testCaseIgnored.entrySet()) { testCaseMessage.append(NEWLINE_INDENT2) .append(entry.getKey()) + .append(": ") //get the reason specified by the ignore file: .append(entry.getValue()); } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index ea6acfc72..848bdd493 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -19,10 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.*; @@ -241,7 +238,6 @@ private static Callable createPostCallable(HttpPost post, PostComponent po * } ] * } * It extracts the diagnostics and returns a string appendable to the response - * */ private static String getDiagnosticString(String jsonString) { try { @@ -343,8 +339,10 @@ public static void postTaskCollection() { //execute the remaining tasks: executeTasks(executorService, tasks); + + reportProgress(); if (failedPostCalls.isEmpty()) { - System.out.println("Retry successful, all tasks successfully posted"); + System.out.println("\r\nRetry successful, all tasks successfully posted"); } } } @@ -367,11 +365,26 @@ public static void postTaskCollection() { Collections.sort(failedMessages); message = new StringBuilder(); + for (String failedPost : failedMessages) { message.append("\n").append(failedPost); } + + message.append("\r\n").append(failedMessages.size()).append(" resources failed to post."); System.out.println(message); + + if (!failedMessages.isEmpty()) { + String httpFailLog = "httpfail.log"; + try (BufferedWriter writer = new BufferedWriter(new FileWriter(httpFailLog))) { + for (String str : failedMessages) { + writer.write(str + "\n"); + } + System.out.println("\r\nRecorded failed POST tasks to log file: " + new File(httpFailLog).getAbsolutePath() + "\r\n"); + } catch (IOException e) { + System.out.println("\r\nRecording of failed POST tasks to log failed with exception: " + e.getMessage() + "\r\n"); + } + } } } finally { From 9321b0f767541422f524a132af957a9941a6c3bf Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 28 Dec 2023 12:35:19 -0500 Subject: [PATCH 16/20] Sorting all summary lists at end of bundle process. Moved summary message generation to its own method. --- .../tooling/processor/AbstractBundler.java | 114 ++++++++++++++---- .../tooling/utilities/HttpClientUtils.java | 66 ++++++++-- 2 files changed, 146 insertions(+), 34 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index 0ce0ab9f3..93fb9b1c5 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -9,14 +9,11 @@ import org.opencds.cqf.tooling.common.ThreadUtils; import org.opencds.cqf.tooling.cql.exception.CqlTranslatorException; import org.opencds.cqf.tooling.library.LibraryProcessor; -import org.opencds.cqf.tooling.measure.MeasureBundler; import org.opencds.cqf.tooling.utilities.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.Callable; @@ -296,22 +293,55 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa LogUtils.putException("bundleResources: " + getResourceBundlerType(), e); } - //Prepare final report: - StringBuilder message = new StringBuilder(NEWLINE); + //Output final report: + System.out.println( + generateBundleProcessSummary(refreshedLibraryNames, fhirContext, fhirUri, verboseMessaging, + persistedFileReport, bundledResources, failedExceptionMessages, cqlTranslatorErrorMessages) + ); + } + + /** + * Generates a summary message based on the processing results of bundling and persisting FHIR resources. + * The summary contains a list of measures that failed as well as which measures have tasks in the post queue. + * All summary lists are sorted for readability. + * + * @param refreshedLibraryNames The list of refreshed library names. + * @param fhirContext The FHIR context used for processing resources. + * @param fhirUri The FHIR server URI for persisting resources. + * @param verboseMessaging A flag indicating whether to include verbose messaging. + * @param persistedFileReport A map containing the count of files queued for each library during persistence. + * @param bundledResources The list of successfully bundled resources. + * @param failedExceptionMessages A map containing exception messages for failed resources. + * @param cqlTranslatorErrorMessages A map containing CQL translator error messages for each library. + * @return A StringBuilder containing the generated summary message. + */ + private StringBuilder generateBundleProcessSummary(ArrayList refreshedLibraryNames, FhirContext fhirContext, + String fhirUri, Boolean verboseMessaging, Map persistedFileReport, + List bundledResources, Map failedExceptionMessages, + Map> cqlTranslatorErrorMessages) { + + StringBuilder summaryMessage = new StringBuilder(NEWLINE); //Give user a snapshot of the files each resource will have persisted to their FHIR server (if fhirUri is provided) final int persistCount = persistedFileReport.size(); if (persistCount > 0) { - message.append(NEWLINE).append(persistCount).append(" ").append(getResourceBundlerType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); + summaryMessage.append(NEWLINE).append(persistCount).append(" ").append(getResourceBundlerType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); int totalQueueCount = 0; + List persistMessages = new ArrayList<>(); for (String library : persistedFileReport.keySet()) { totalQueueCount = totalQueueCount + persistedFileReport.get(library); - message.append(NEWLINE_INDENT) - .append(persistedFileReport.get(library)) - .append(" File(s): ") - .append(library); + persistMessages.add(NEWLINE_INDENT + + persistedFileReport.get(library) + + " File(s): " + + library); } - message.append(NEWLINE_INDENT) + + persistMessages.sort(new FileComparator()); + + for (String persistMessage : persistMessages) { + summaryMessage.append(persistMessage); + } + summaryMessage.append(NEWLINE_INDENT) .append("Total: ") .append(totalQueueCount) .append(" File(s)"); @@ -320,9 +350,14 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final int bundledCount = bundledResources.size(); if (bundledCount > 0) { - message.append(NEWLINE).append(bundledCount).append(" ").append(getResourceBundlerType()).append("(s) successfully bundled:"); + summaryMessage.append(NEWLINE).append(bundledCount).append(" ").append(getResourceBundlerType()).append("(s) successfully bundled:"); + List bundledMessages = new ArrayList<>(); for (String bundledResource : bundledResources) { - message.append(NEWLINE_INDENT).append(bundledResource).append(" BUNDLED"); + bundledMessages.add(NEWLINE_INDENT + bundledResource + " BUNDLED"); + } + Collections.sort(bundledMessages); + for (String bundledMessage : bundledMessages) { + summaryMessage.append(bundledMessage); } } @@ -333,12 +368,16 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa List failedResources = new ArrayList<>(resourcePathLibraryNames); resourcePathLibraryNames.removeAll(bundledResources); resourcePathLibraryNames.retainAll(refreshedLibraryNames); - final int refreshedNotBundledCount = resourcePathLibraryNames.size(); if (refreshedNotBundledCount > 0) { - message.append(NEWLINE).append(refreshedNotBundledCount).append(" ").append(getResourceBundlerType()).append("(s) refreshed, but not bundled (due to issues):"); + List refreshNotBundledMessages = new ArrayList<>(); + summaryMessage.append(NEWLINE).append(refreshedNotBundledCount).append(" ").append(getResourceBundlerType()).append("(s) refreshed, but not bundled (due to issues):"); for (String notBundled : resourcePathLibraryNames) { - message.append(NEWLINE_INDENT).append(notBundled).append(" REFRESHED"); + refreshNotBundledMessages.add(NEWLINE_INDENT + notBundled + " REFRESHED"); + } + Collections.sort(refreshNotBundledMessages); + for (String refreshNotBundledMessage : refreshNotBundledMessages) { + summaryMessage.append(refreshNotBundledMessage); } } @@ -348,31 +387,58 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final int failedCount = failedResources.size(); if (failedCount > 0) { - message.append(NEWLINE).append(failedCount).append(" ").append(getResourceBundlerType()).append("(s) failed refresh:"); + List failedMessages = new ArrayList<>(); + summaryMessage.append(NEWLINE).append(failedCount).append(" ").append(getResourceBundlerType()).append("(s) failed refresh:"); for (String failed : failedResources) { - if (failedExceptionMessages.containsKey(failed) && verboseMessaging) { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED: ").append(failedExceptionMessages.get(failed)); + String failMessage = NEWLINE_INDENT + failed + " FAILED"; + if (verboseMessaging && failedExceptionMessages.containsKey(failed)) { + failedMessages.add(failMessage + ": " + failedExceptionMessages.get(failed)); } else { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED"); + failedMessages.add(failMessage); } } + Collections.sort(failedMessages); + for (String failMessage : failedMessages) { + summaryMessage.append(failMessage); + } } //Exceptions stemming from IOUtils.translate that did not prevent process from completing for file: final int translateErrorCount = cqlTranslatorErrorMessages.size(); if (translateErrorCount > 0) { - message.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceBundlerType()).append("(s) encountered CQL translator errors:"); - + List translateErrorMessages = new ArrayList<>(); + summaryMessage.append(NEWLINE).append(cqlTranslatorErrorMessages.size()).append(" ").append(getResourceBundlerType()).append("(s) encountered CQL translator errors:"); for (String library : cqlTranslatorErrorMessages.keySet()) { - message.append(NEWLINE_INDENT).append( + translateErrorMessages.add(NEWLINE_INDENT + CqlProcessor.buildStatusMessage(cqlTranslatorErrorMessages.get(library), library, verboseMessaging, false, NEWLINE_INDENT2) ); } + Collections.sort(translateErrorMessages); + for (String translateErrorMessage : translateErrorMessages) { + summaryMessage.append(translateErrorMessage); + } } - System.out.println(message); + return summaryMessage; } + private class FileComparator implements Comparator { + @Override + public int compare(String file1, String file2) { + int count1 = fileCount(file1); + int count2 = fileCount(file2); + return Integer.compare(count1, count2); + } + + private int fileCount(String fileName) { + int endIndex = fileName.indexOf(" File(s):"); + if (endIndex != -1) { + String countString = fileName.substring(0, endIndex).trim(); + return Integer.parseInt(countString); + } + return 0; + } + } private void reportProgress(int count, int total) { double percentage = (double) count / total * 100; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index 848bdd493..c3a5e0b6b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -3,14 +3,17 @@ import ca.uhn.fhir.context.FhirContext; import com.google.gson.JsonParser; import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -20,6 +23,7 @@ import org.slf4j.LoggerFactory; import java.io.*; +import java.net.URI; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.*; @@ -212,13 +216,43 @@ private static Callable createPostCallable(HttpPost post, PostComponent po String diagnosticString = getDiagnosticString(EntityUtils.toString(response.getEntity())); if (statusCode >= 200 && statusCode < 300) { - successfulPostCalls.add("[SUCCESS] Resource successfully posted to " + postComponent.fhirServerUrl + ": " + resourceIdentifier); + successfulPostCalls.add(buildSuccessMessage(postComponent.fhirServerUrl, resourceIdentifier)); + }else if (statusCode == 301){ + //redirected, find new location: + Header locationHeader = response.getFirstHeader("Location"); + if (locationHeader != null) { + postComponent.redirectFhirServerUrl = locationHeader.getValue(); + HttpPost redirectedPost = configureHttpPost(postComponent.redirectFhirServerUrl, postComponent.resource, postComponent.encoding, postComponent.fhirContext); + String redirectLocationIdentifier = postComponent.redirectFhirServerUrl + + "(redirected from " + postComponent.fhirServerUrl + ")"; + //attempt to post at location specified in redirect response: + try (CloseableHttpClient redirectHttpClient = HttpClientBuilder.create().build()) { + HttpResponse redirectResponse = redirectHttpClient.execute(redirectedPost); + StatusLine redirectStatusLine = redirectResponse.getStatusLine(); + int redirectStatusCode = redirectStatusLine.getStatusCode(); + String redirectDiagnosticString = getDiagnosticString(EntityUtils.toString(redirectResponse.getEntity())); + + //treat new response same as we would before: + if (redirectStatusCode >= 200 && redirectStatusCode < 300) { + successfulPostCalls.add(buildSuccessMessage(redirectLocationIdentifier, resourceIdentifier)); + } else { + failedPostCalls.add(buildFailedPostMessage(postComponent, redirectStatusCode, redirectLocationIdentifier, resourceIdentifier, redirectDiagnosticString)); + } + } catch (Exception e) { + failedPostCalls.add(buildExceptionMessage(postComponent, e, resourceIdentifier, redirectLocationIdentifier)); + } + + } else { + //failed to extract a location from redirect message: + failedPostCalls.add(Pair.of("[FAIL] Exception during " + resourceIdentifier + " POST request execution to " + + postComponent.fhirServerUrl + ": Redirect, but no new location specified", postComponent)); + } } else { - failedPostCalls.add(Pair.of("[FAIL] Error " + statusCode + " from " + postComponent.fhirServerUrl + ": " + resourceIdentifier + ": " + diagnosticString, postComponent)); + failedPostCalls.add(buildFailedPostMessage(postComponent, statusCode, postComponent.fhirServerUrl, resourceIdentifier, diagnosticString)); } } catch (Exception e) { - failedPostCalls.add(Pair.of("[FAIL] Exception during " + resourceIdentifier + " POST request execution to " + postComponent.fhirServerUrl + ": " + e.getMessage(), postComponent)); + failedPostCalls.add(buildExceptionMessage(postComponent, e, resourceIdentifier, postComponent.fhirServerUrl)); } runningPostTaskList.remove(postComponent.resource); @@ -227,6 +261,18 @@ private static Callable createPostCallable(HttpPost post, PostComponent po }; } + private static Pair buildExceptionMessage(PostComponent postComponent, Exception e, String resourceIdentifier, String locationIdentifier) { + return Pair.of("[FAIL] Exception during " + resourceIdentifier + " POST request execution to " + locationIdentifier + ": " + e.getMessage(), postComponent); + } + + private static Pair buildFailedPostMessage(PostComponent postComponent, int statusCode, String locationIdentifier, String resourceIdentifier, String diagnosticString) { + return Pair.of("[FAIL] Error " + statusCode + " from " + locationIdentifier + ": " + resourceIdentifier + ": " + diagnosticString, postComponent); + } + + private static String buildSuccessMessage(String locationIdentifier, String resourceIdentifier) { + return "[SUCCESS] Resource successfully posted to " + locationIdentifier + ": " + resourceIdentifier; + } + /** * This method takes in a json string from the endpoint that might look like this: * { @@ -477,13 +523,13 @@ public static String getResponse(HttpResponse response) throws IOException { * It includes the FHIR server URL, the FHIR resource to be posted, the encoding type, and the FHIR context. */ private static class PostComponent { - String fhirServerUrl; - IBaseResource resource; - IOUtils.Encoding encoding; - FhirContext fhirContext; - String fileLocation; - boolean hasPriority; - + private final String fhirServerUrl; + private String redirectFhirServerUrl; + private final IBaseResource resource; + private final IOUtils.Encoding encoding; + private final FhirContext fhirContext; + private final String fileLocation; + private final boolean hasPriority; public PostComponent(String fhirServerUrl, IBaseResource resource, IOUtils.Encoding encoding, FhirContext fhirContext, String fileLocation, boolean hasPriority) { this.fhirServerUrl = fhirServerUrl; this.resource = resource; From 7df3885a7ff2cad3f3c15e4778b7ab45b94968fe Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Sat, 30 Dec 2023 09:21:53 -0500 Subject: [PATCH 17/20] applying a timestamp to the httpfail log filename --- .../tooling/processor/AbstractBundler.java | 39 ++++++++++--------- .../tooling/utilities/HttpClientUtils.java | 36 ++++++++++------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index 93fb9b1c5..195050508 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -336,7 +336,7 @@ private StringBuilder generateBundleProcessSummary(ArrayList refreshedLi + library); } - persistMessages.sort(new FileComparator()); + persistMessages.sort(new FileCountComparator()); for (String persistMessage : persistMessages) { summaryMessage.append(persistMessage); @@ -422,24 +422,6 @@ private StringBuilder generateBundleProcessSummary(ArrayList refreshedLi return summaryMessage; } - private class FileComparator implements Comparator { - @Override - public int compare(String file1, String file2) { - int count1 = fileCount(file1); - int count2 = fileCount(file2); - return Integer.compare(count1, count2); - } - - private int fileCount(String fileName) { - int endIndex = fileName.indexOf(" File(s):"); - if (endIndex != -1) { - String countString = fileName.substring(0, endIndex).trim(); - return Integer.parseInt(countString); - } - return 0; - } - } - private void reportProgress(int count, int total) { double percentage = (double) count / total * 100; System.out.print("\rBundle " + getResourceBundlerType() + "s: " + String.format("%.2f%%", percentage) + " processed."); @@ -511,5 +493,24 @@ private void bundleFiles(String igPath, String bundleDestPath, String primaryLib } + /** + * Simple comparator for sorting the post queue file count list: + */ + private static class FileCountComparator implements Comparator { + @Override + public int compare(String file1, String file2) { + int count1 = fileCount(file1); + int count2 = fileCount(file2); + return Integer.compare(count1, count2); + } + private int fileCount(String fileName) { + int endIndex = fileName.indexOf(" File(s):"); + if (endIndex != -1) { + String countString = fileName.substring(0, endIndex).trim(); + return Integer.parseInt(countString); + } + return 0; + } + } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index c3a5e0b6b..1dd1333eb 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -10,10 +10,8 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -23,8 +21,8 @@ import org.slf4j.LoggerFactory; import java.io.*; -import java.net.URI; import java.nio.file.Paths; +import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.*; import java.util.function.Function; @@ -420,17 +418,7 @@ public static void postTaskCollection() { message.append("\r\n").append(failedMessages.size()).append(" resources failed to post."); System.out.println(message); - if (!failedMessages.isEmpty()) { - String httpFailLog = "httpfail.log"; - try (BufferedWriter writer = new BufferedWriter(new FileWriter(httpFailLog))) { - for (String str : failedMessages) { - writer.write(str + "\n"); - } - System.out.println("\r\nRecorded failed POST tasks to log file: " + new File(httpFailLog).getAbsolutePath() + "\r\n"); - } catch (IOException e) { - System.out.println("\r\nRecording of failed POST tasks to log failed with exception: " + e.getMessage() + "\r\n"); - } - } + writeFailedPostAttemptsToLog(failedMessages); } } finally { @@ -439,6 +427,26 @@ public static void postTaskCollection() { } } + /** + * Gives the user a log file containing failed POST attempts during postTaskCollection() + * @param failedMessages + */ + private static void writeFailedPostAttemptsToLog(List failedMessages) { + if (!failedMessages.isEmpty()) { + //generate a unique filename based on simple timestamp: + String httpFailLogFilename = "http_post_fail_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".log"; + try (BufferedWriter writer = new BufferedWriter(new FileWriter(httpFailLogFilename))) { + for (String str : failedMessages) { + writer.write(str + "\n"); + } + System.out.println("\r\nRecorded failed POST tasks to log file: " + new File(httpFailLogFilename).getAbsolutePath() + "\r\n"); + } catch (IOException e) { + System.out.println("\r\nRecording of failed POST tasks to log failed with exception: " + e.getMessage() + "\r\n"); + } + } + } + + private static void executeTasks(ExecutorService executorService, Map> executableTasksMap) { List> futures = new ArrayList<>(); List resources = new ArrayList<>(executableTasksMap.keySet()); From 21cd22674f682954962dc549882eb0828550dea6 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Mon, 1 Jan 2024 22:26:57 -0500 Subject: [PATCH 18/20] Sorting the test case refresh summary for readability. --- .../tooling/processor/AbstractBundler.java | 43 +++++++++---------- .../tooling/processor/TestCaseProcessor.java | 19 ++++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index 195050508..268f5354f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -325,6 +325,7 @@ private StringBuilder generateBundleProcessSummary(ArrayList refreshedLi //Give user a snapshot of the files each resource will have persisted to their FHIR server (if fhirUri is provided) final int persistCount = persistedFileReport.size(); if (persistCount > 0) { + String fileDisplay = " File(s): "; summaryMessage.append(NEWLINE).append(persistCount).append(" ").append(getResourceBundlerType()).append("(s) have POST tasks in the queue for ").append(fhirUri).append(": "); int totalQueueCount = 0; List persistMessages = new ArrayList<>(); @@ -332,11 +333,28 @@ private StringBuilder generateBundleProcessSummary(ArrayList refreshedLi totalQueueCount = totalQueueCount + persistedFileReport.get(library); persistMessages.add(NEWLINE_INDENT + persistedFileReport.get(library) - + " File(s): " + + fileDisplay + library); } - persistMessages.sort(new FileCountComparator()); + //anon comparator class to sort by the file count for better presentation + persistMessages.sort(new Comparator<>() { + @Override + public int compare(String displayFileCount1, String displayFileCount2) { + int count1 = getFileCountFromString(displayFileCount1); + int count2 = getFileCountFromString(displayFileCount2); + return Integer.compare(count1, count2); + } + + private int getFileCountFromString(String fileName) { + int endIndex = fileName.indexOf(fileDisplay); + if (endIndex != -1) { + String countString = fileName.substring(0, endIndex).trim(); + return Integer.parseInt(countString); + } + return 0; + } + }); for (String persistMessage : persistMessages) { summaryMessage.append(persistMessage); @@ -492,25 +510,4 @@ private void bundleFiles(String igPath, String bundleDestPath, String primaryLib } } - - /** - * Simple comparator for sorting the post queue file count list: - */ - private static class FileCountComparator implements Comparator { - @Override - public int compare(String file1, String file2) { - int count1 = fileCount(file1); - int count2 = fileCount(file2); - return Integer.compare(count1, count2); - } - - private int fileCount(String fileName) { - int endIndex = fileName.indexOf(" File(s):"); - if (endIndex != -1) { - String countString = fileName.substring(0, endIndex).trim(); - return Integer.parseInt(countString); - } - return 0; - } - } } \ No newline at end of file diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index 8adf44d18..b98e42f49 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -15,10 +15,7 @@ import org.opencds.cqf.tooling.utilities.ResourceUtils; import java.io.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -218,6 +215,8 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext } } + reportProgress((testCaseRefreshSuccessMap.size() + testCaseRefreshFailMap.size()), totalTestFileCount); + StringBuilder testCaseMessage = buildInformationMessage(testCaseRefreshFailMap, testCaseRefreshSuccessMap, "Test Case", "Refreshed", verboseMessaging); if (!groupFileRefreshSuccessMap.isEmpty() || !groupFileRefreshFailMap.isEmpty()) { @@ -276,14 +275,18 @@ private StringBuilder buildInformationMessage(Map failMap, Map successKeys = new ArrayList<>(successMap.keySet()); + Collections.sort(successKeys); + for (String successCase : successKeys) { + message.append(NEWLINE_INDENT).append(successCase).append(" ").append(successType.toUpperCase()); } } if (!failMap.isEmpty()) { message.append(NEWLINE).append(failMap.size()).append(" ").append(type).append("(s) failed to be ").append(successType.toLowerCase()).append(":"); - for (String failed : failMap.keySet()) { - message.append(NEWLINE_INDENT).append(failed).append(" FAILED").append(verboseMessaging ? ": " + failMap.get(failed) : ""); + List failKeys = new ArrayList<>(failMap.keySet()); + Collections.sort(failKeys); + for (String failEntry : failKeys) { + message.append(NEWLINE_INDENT).append(failEntry).append(" FAILED").append(verboseMessaging ? ": " + failMap.get(failEntry) : ""); } } return message; From cb6ef878e1fe2874dd614ff4d83f952a2c0cbad0 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Thu, 4 Jan 2024 13:28:46 -0500 Subject: [PATCH 19/20] Using SLF4J provider ch.qos.logback.classic.spi.LogbackServiceProvider so that warnings from ca.uhn.fhir.parser.LenientErrorHandler can be supressed during various operations. We already handle the translator error list. --- tooling-cli/pom.xml | 14 ++++++++------ .../java/org/opencds/cqf/tooling/cli/Main.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tooling-cli/pom.xml b/tooling-cli/pom.xml index 10e639548..103287dca 100644 --- a/tooling-cli/pom.xml +++ b/tooling-cli/pom.xml @@ -16,6 +16,14 @@ CQF Tooling CLI + + + + ch.qos.logback + logback-classic + 1.4.14 + + org.opencds.cqf tooling @@ -32,12 +40,6 @@ model-jackson - - org.slf4j - slf4j-simple - compile - - org.reflections reflections 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 1cfccabfd..2ea2ee8aa 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 @@ -230,6 +230,9 @@ //import org.opencds.cqf.tooling.operations.ExecutableOperation; //import org.opencds.cqf.tooling.operations.Operation; //import org.reflections.Reflections; + +import ca.uhn.fhir.parser.LenientErrorHandler; +import ch.qos.logback.classic.Level; import org.opencds.cqf.tooling.common.ThreadUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -279,6 +282,14 @@ public class Main { // } public static void main(String[] args) { + + //ca.uhn.fhir.parser.LenientErrorHandler warning suppression: + try { + suppressLogsFromLenientErrorHandler(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + //ensure any and all executors are shutdown cleanly when app is shutdown: Runtime.getRuntime().addShutdownHook(new Thread(ThreadUtils::shutdownRunningExecutors)); @@ -295,4 +306,9 @@ public static void main(String[] args) { OperationFactory.createOperation(operation.substring(1)).execute(args); } + + private static void suppressLogsFromLenientErrorHandler() throws Exception { + Logger logger = LoggerFactory.getLogger(LenientErrorHandler.class); + logger.getClass().getMethod("setLevel", Level.class).invoke(logger, Level.ERROR); + } } From 69091a76d2e8c2d61807aefcba05492df1ecc082 Mon Sep 17 00:00:00 2001 From: Evan Chicoine Date: Fri, 1 Mar 2024 14:17:40 -0500 Subject: [PATCH 20/20] Using logger over System.out --- tooling-cli/pom.xml | 8 +- .../org/opencds/cqf/tooling/cli/Main.java | 15 - .../cqf/tooling/library/LibraryProcessor.java | 2 +- .../cqf/tooling/measure/MeasureProcessor.java | 4 +- .../tooling/operation/BundleResources.java | 9 +- .../operation/PostmanCollectionOperation.java | 6 +- .../tooling/operation/RefreshIGOperation.java | 6 +- .../tooling/processor/AbstractBundler.java | 15 +- .../cqf/tooling/processor/BaseProcessor.java | 2 +- .../cqf/tooling/processor/CqlProcessor.java | 2 +- .../tooling/processor/IGBundleProcessor.java | 2 +- .../tooling/processor/IGTestProcessor.java | 4 +- .../tooling/processor/TestCaseProcessor.java | 6 +- .../tooling/utilities/HttpClientUtils.java | 20 +- .../cqf/tooling/utilities/IOUtils.java | 2 +- .../Library-LibraryEvaluationTest.json | 119 +++++++ ...Library-LibraryEvaluationTestConcepts.json | 26 ++ ...brary-LibraryEvaluationTestDependency.json | 64 ++++ .../Questionnaire-libraryevaluationtest.json | 328 ++++++++++++++++++ ...ueSet-condition-problem-list-category.json | 21 ++ tooling/tooling-cli/results/AdverseEvent.html | 74 ++++ .../results/AllergyIntolerance.html | 72 ++++ tooling/tooling-cli/results/Assessment.html | 193 +++++++++++ .../tooling-cli/results/CareExperience.html | 90 +++++ tooling/tooling-cli/results/CareGoal.html | 64 ++++ .../tooling-cli/results/Communication.html | 223 ++++++++++++ .../results/ConditionDiagnosisProblem.html | 64 ++++ tooling/tooling-cli/results/Device.html | 164 +++++++++ .../tooling-cli/results/DiagnosticStudy.html | 266 ++++++++++++++ tooling/tooling-cli/results/Encounter.html | 187 ++++++++++ .../tooling-cli/results/FamilyHistory.html | 50 +++ tooling/tooling-cli/results/Immunization.html | 131 +++++++ .../results/IndividualCharacteristic.html | 242 +++++++++++++ tooling/tooling-cli/results/Intervention.html | 315 +++++++++++++++++ .../tooling-cli/results/LaboratoryTest.html | 199 +++++++++++ tooling/tooling-cli/results/Medication.html | 316 +++++++++++++++++ .../tooling-cli/results/Participation.html | 43 +++ tooling/tooling-cli/results/PhysicalExam.html | 199 +++++++++++ tooling/tooling-cli/results/Procedure.html | 210 +++++++++++ 39 files changed, 3708 insertions(+), 55 deletions(-) create mode 100644 tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTest.json create mode 100644 tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestConcepts.json create mode 100644 tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestDependency.json create mode 100644 tooling/tooling-cli/bundleResourcesResults/Questionnaire-libraryevaluationtest.json create mode 100644 tooling/tooling-cli/bundleResourcesResults/ValueSet-condition-problem-list-category.json create mode 100644 tooling/tooling-cli/results/AdverseEvent.html create mode 100644 tooling/tooling-cli/results/AllergyIntolerance.html create mode 100644 tooling/tooling-cli/results/Assessment.html create mode 100644 tooling/tooling-cli/results/CareExperience.html create mode 100644 tooling/tooling-cli/results/CareGoal.html create mode 100644 tooling/tooling-cli/results/Communication.html create mode 100644 tooling/tooling-cli/results/ConditionDiagnosisProblem.html create mode 100644 tooling/tooling-cli/results/Device.html create mode 100644 tooling/tooling-cli/results/DiagnosticStudy.html create mode 100644 tooling/tooling-cli/results/Encounter.html create mode 100644 tooling/tooling-cli/results/FamilyHistory.html create mode 100644 tooling/tooling-cli/results/Immunization.html create mode 100644 tooling/tooling-cli/results/IndividualCharacteristic.html create mode 100644 tooling/tooling-cli/results/Intervention.html create mode 100644 tooling/tooling-cli/results/LaboratoryTest.html create mode 100644 tooling/tooling-cli/results/Medication.html create mode 100644 tooling/tooling-cli/results/Participation.html create mode 100644 tooling/tooling-cli/results/PhysicalExam.html create mode 100644 tooling/tooling-cli/results/Procedure.html diff --git a/tooling-cli/pom.xml b/tooling-cli/pom.xml index 126fd7acc..a7fb2ec28 100644 --- a/tooling-cli/pom.xml +++ b/tooling-cli/pom.xml @@ -16,12 +16,10 @@ CQF Tooling CLI - - - ch.qos.logback - logback-classic - 1.4.14 + org.slf4j + slf4j-simple + compile 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 3d42b8878..872832fec 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 @@ -237,8 +237,6 @@ //import org.opencds.cqf.tooling.operations.Operation; //import org.reflections.Reflections; -import ca.uhn.fhir.parser.LenientErrorHandler; -import ch.qos.logback.classic.Level; import org.opencds.cqf.tooling.common.ThreadUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -288,14 +286,6 @@ public class Main { // } public static void main(String[] args) { - - //ca.uhn.fhir.parser.LenientErrorHandler warning suppression: - try { - suppressLogsFromLenientErrorHandler(); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - //ensure any and all executors are shutdown cleanly when app is shutdown: Runtime.getRuntime().addShutdownHook(new Thread(ThreadUtils::shutdownRunningExecutors)); @@ -312,9 +302,4 @@ public static void main(String[] args) { OperationFactory.createOperation(operation.substring(1)).execute(args); } - - private static void suppressLogsFromLenientErrorHandler() throws Exception { - Logger logger = LoggerFactory.getLogger(LenientErrorHandler.class); - logger.getClass().getMethod("setLevel", Level.class).invoke(logger, Level.ERROR); - } } 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 71d2b449e..f8ceccc79 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 @@ -78,7 +78,7 @@ public List refreshIgLibraryContent(BaseProcessor parentContext, Encodin } public List refreshIgLibraryContent(BaseProcessor parentContext, Encoding outputEncoding, String libraryPath, String libraryOutputDirectory, Boolean versioned, FhirContext fhirContext, Boolean shouldApplySoftwareSystemStamp) { - System.out.println("\r\n[Refreshing Libraries]\r\n"); + logger.info("[Refreshing Libraries]"); LibraryProcessor libraryProcessor; switch (fhirContext.getVersion().getVersion()) { diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java index a1bae907a..86f462aa3 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureProcessor.java @@ -38,7 +38,7 @@ public List refreshIgMeasureContent(BaseProcessor parentContext, Encodin Boolean versioned, FhirContext fhirContext, String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp) { - System.out.println("\r\n[Refreshing Measures]\r\n"); + logger.info("[Refreshing Measures]"); MeasureProcessor measureProcessor; switch (fhirContext.getVersion().getVersion()) { @@ -113,7 +113,7 @@ private Measure refreshGeneratedContent(Measure measure, MeasureRefreshProcessor List errors = new CopyOnWriteArrayList<>(); CompiledLibrary CompiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors); - System.out.println(CqlProcessor.buildStatusMessage(errors, measure.getName(), verboseMessaging)); + logger.info(CqlProcessor.buildStatusMessage(errors, measure.getName(), verboseMessaging)); boolean hasSevereErrors = CqlProcessor.hasSevereErrors(errors); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleResources.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleResources.java index 82fced60f..d4e02e38f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleResources.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/BundleResources.java @@ -10,13 +10,16 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.tooling.Operation; +import org.opencds.cqf.tooling.processor.BaseProcessor; import org.opencds.cqf.tooling.utilities.IOUtils; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BundleResources extends Operation { - + private final static Logger logger = LoggerFactory.getLogger(BundleResources.class); private String encoding; // -encoding (-e) private String pathToDirectory; // -pathtodir (-ptd) private String version; // -version (-v) Can be dstu2, stu3, or @@ -165,7 +168,7 @@ private void getResources(File[] resources) { } catch (Exception e) { String message = String.format("'%s' will not be included in the bundle because the following error occurred: '%s'", resource.getName(), e.getMessage()); - System.out.println(message); + logger.error(message, e); continue; } } @@ -178,7 +181,7 @@ else if (resource.getPath().endsWith(".json")) { } catch (Exception e) { String message = String.format("'%s' will not be included in the bundle because the following error occurred: '%s'", resource.getName(), e.getMessage()); - System.out.println(message); + logger.error(message, e); continue; } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/PostmanCollectionOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/PostmanCollectionOperation.java index a4c456ef0..906340b27 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/PostmanCollectionOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/PostmanCollectionOperation.java @@ -32,9 +32,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ca.uhn.fhir.context.FhirContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PostmanCollectionOperation extends Operation { - + private final static Logger logger = LoggerFactory.getLogger(PostmanCollectionOperation.class); private static final String POSTMAN_COLLECTION_SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"; private String pathToBundlesDir; private FhirContext context; @@ -512,7 +514,7 @@ private IBaseResource parseBundle(File resource) { catch (Exception e) { e.printStackTrace(); String message = String.format("'%s' will not be included in the bundle because the following error occurred: '%s'", resource.getName(), e.getMessage()); - System.out.println(message); + logger.error(message, e); } return theResource; } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java index 7708f3269..877c9d2c5 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/RefreshIGOperation.java @@ -4,9 +4,11 @@ import org.opencds.cqf.tooling.parameter.RefreshIGParameters; import org.opencds.cqf.tooling.processor.IGProcessor; import org.opencds.cqf.tooling.processor.argument.RefreshIGArgumentProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RefreshIGOperation extends Operation { - + private final static Logger logger = LoggerFactory.getLogger(RefreshIGOperation.class); public RefreshIGOperation() { } @@ -27,7 +29,7 @@ public void execute(String[] args) { } if (params.verboseMessaging == null || !params.verboseMessaging) { - System.out.println("\r\nRe-run with -x to for expanded reporting of errors, warnings, and informational messages.\r\n"); + logger.info("Re-run with -x to for expanded reporting of errors, warnings, and informational messages."); } new IGProcessor().publishIG(params); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java index 268f5354f..3f76367e1 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/AbstractBundler.java @@ -34,7 +34,7 @@ public abstract class AbstractBundler { /** * The logger for logging messages specific to the implementing class. */ - protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + protected final static Logger logger = LoggerFactory.getLogger(AbstractBundler.class); /** * The resource type constant for Questionnaire. @@ -116,7 +116,7 @@ private String getTime() { public void bundleResources(ArrayList refreshedLibraryNames, String igPath, List binaryPaths, Boolean includeDependencies, Boolean includeTerminology, Boolean includePatientScenarios, Boolean includeVersion, Boolean addBundleTimestamp, FhirContext fhirContext, String fhirUri, IOUtils.Encoding encoding, Boolean verboseMessaging) { - System.out.println("\r\n[Bundling " + getResourceBundlerType() + "s]\r\n"); + logger.info("\r\n[Bundling " + getResourceBundlerType() + "s]\r\n"); final List bundledResources = new CopyOnWriteArrayList<>(); @@ -142,7 +142,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa final Map libraryPathMap = new ConcurrentHashMap<>(IOUtils.getLibraryPathMap(fhirContext)); if (resourcesMap.isEmpty()) { - System.out.println("[INFO] No " + getResourceBundlerType() + "s found. Continuing..."); + logger.info("[INFO] No " + getResourceBundlerType() + "s found. Continuing..."); return; } @@ -169,7 +169,7 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa tasks.add(() -> { //check if resourceSourcePath has been processed before: if (processedResources.contains(resourceSourcePath)) { - System.out.println(getResourceBundlerType() + " processed already: " + resourceSourcePath); + logger.info(getResourceBundlerType() + " processed already: " + resourceSourcePath); return null; } String resourceName = FilenameUtils.getBaseName(resourceSourcePath).replace(getResourcePrefix(), ""); @@ -294,10 +294,9 @@ public void bundleResources(ArrayList refreshedLibraryNames, String igPa } //Output final report: - System.out.println( - generateBundleProcessSummary(refreshedLibraryNames, fhirContext, fhirUri, verboseMessaging, - persistedFileReport, bundledResources, failedExceptionMessages, cqlTranslatorErrorMessages) - ); + String summaryOutput = generateBundleProcessSummary(refreshedLibraryNames, fhirContext, fhirUri, verboseMessaging, + persistedFileReport, bundledResources, failedExceptionMessages, cqlTranslatorErrorMessages).toString(); + logger.info(summaryOutput); } /** diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java index ed1d68203..121a32207 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/BaseProcessor.java @@ -29,7 +29,7 @@ public class BaseProcessor implements IProcessorContext, IWorkerContext.ILoggingService { - private static final Logger logger = LoggerFactory.getLogger(BaseProcessor.class); + protected static final Logger logger = LoggerFactory.getLogger(BaseProcessor.class); protected String rootDir; diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java index 14e67cbeb..9621c0b3b 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/CqlProcessor.java @@ -409,7 +409,7 @@ private void translateFile(LibraryManager libraryManager, File file, CqlCompiler } //output Success/Warn/Info/Fail message to user: - System.out.println(buildStatusMessage(translator.getErrors(), file.getName(), verboseMessaging)); + logger.logMessage(buildStatusMessage(translator.getErrors(), file.getName(), verboseMessaging)); } catch (Exception e) { result.getErrors().add(new ValidationMessage(ValidationMessage.Source.Publisher, IssueType.EXCEPTION, file.getName(), "CQL Processing failed with exception: "+e.getMessage(), IssueSeverity.ERROR)); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java index 8a2c9b20b..f578e391a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/IGBundleProcessor.java @@ -49,7 +49,7 @@ public void bundleIg(ArrayList refreshedLibraryNames, String igPath, Lis //run collected post calls last: if (HttpClientUtils.hasPostTasksInQueue()) { - System.out.println("\r\n[Persisting Files to " + fhirUri + "]\r\n"); + logger.info("[Persisting Files to " + fhirUri + "]"); HttpClientUtils.postTaskCollection(); } 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 009f290a6..f59a54093 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 @@ -172,7 +172,7 @@ public void testIg(TestIGParameters params) { CqfmSoftwareSystem testTargetSoftwareSystem = getCqfRulerSoftwareSystem(params.fhirServerUri); - System.out.println("\r\n[Running IG Test Cases]\r\n"); + logger.info("[Running IG Test Cases]"); File testCasesDirectory = new File(params.testCasesPath); if (!testCasesDirectory.isDirectory()) { @@ -180,7 +180,7 @@ public void testIg(TestIGParameters params) { } // refresh/generate test bundles - System.out.println("\r\n[Refreshing Test Cases]\r\n"); + logger.info("[Refreshing Test Cases]"); TestCaseProcessor testCaseProcessor = new TestCaseProcessor(); testCaseProcessor.refreshTestCases(params.testCasesPath, IOUtils.Encoding.JSON, fhirContext, verboseMessaging); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java index b98e42f49..99585e0fe 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/processor/TestCaseProcessor.java @@ -10,9 +10,12 @@ import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Reference; +import org.opencds.cqf.tooling.operation.RefreshIGOperation; import org.opencds.cqf.tooling.utilities.BundleUtils; import org.opencds.cqf.tooling.utilities.IOUtils; import org.opencds.cqf.tooling.utilities.ResourceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.*; import java.util.*; @@ -21,6 +24,7 @@ import java.util.stream.Collectors; public class TestCaseProcessor { + private final static Logger logger = LoggerFactory.getLogger(TestCaseProcessor.class); public static final String NEWLINE_INDENT = "\r\n\t"; public static final String NEWLINE_INDENT2 = "\r\n\t\t"; public static final String NEWLINE = "\r\n"; @@ -62,7 +66,7 @@ public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext public void refreshTestCases(String path, IOUtils.Encoding encoding, FhirContext fhirContext, @Nullable List refreshedResourcesNames, Boolean verboseMessaging) { - System.out.println("\r\n[Refreshing Tests]\r\n"); + logger.info("[Refreshing Tests]"); final Map testCaseRefreshSuccessMap = new HashMap<>(); final Map testCaseRefreshFailMap = new HashMap<>(); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java index 1dd1333eb..5f10fc86f 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/HttpClientUtils.java @@ -83,7 +83,7 @@ public static void post(String fhirServerUrl, IBaseResource resource, IOUtils.En if (!missingValues.isEmpty()) { String missingValueString = String.join(", ", missingValues); - System.out.println("An invalid HTTP POST call was attempted with a null value for: " + missingValueString + + logger.error("An invalid HTTP POST call was attempted with a null value for: " + missingValueString + (!values.isEmpty() ? "\\nRemaining values are: " + String.join(", ", values) : "")); return; } @@ -333,7 +333,7 @@ public static void postTaskCollection() { ExecutorService executorService = Executors.newFixedThreadPool(1); try { - System.out.println(getTotalTaskCount() + " POST calls to be made. Starting now. Please wait..."); + logger.info(getTotalTaskCount() + " POST calls to be made. Starting now. Please wait..."); double percentage = 0; System.out.print("\rPOST: " + String.format("%.2f%%", percentage) + " done. "); @@ -345,7 +345,7 @@ public static void postTaskCollection() { reportProgress(); - System.out.println("Processing results..."); + logger.info("Processing results..."); Collections.sort(successfulPostCalls); StringBuilder message = new StringBuilder(); @@ -353,11 +353,11 @@ public static void postTaskCollection() { message.append("\n").append(successPost); } message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); - System.out.println(message); + logger.info(message.toString()); successfulPostCalls = new ArrayList<>(); if (!failedPostCalls.isEmpty()) { - System.out.println(failedPostCalls.size() + " tasks failed to POST. Retry these failed posts? (Y/N)"); + logger.info(failedPostCalls.size() + " tasks failed to POST. Retry these failed posts? (Y/N)"); Scanner scanner = new Scanner(System.in); String userInput = scanner.nextLine().trim().toLowerCase(); @@ -386,7 +386,7 @@ public static void postTaskCollection() { reportProgress(); if (failedPostCalls.isEmpty()) { - System.out.println("\r\nRetry successful, all tasks successfully posted"); + logger.info("\r\nRetry successful, all tasks successfully posted"); } } } @@ -397,7 +397,7 @@ public static void postTaskCollection() { message.append("\n").append(successPost); } message.append("\r\n").append(successfulPostCalls.size()).append(" resources successfully posted."); - System.out.println(message); + logger.info(message.toString()); successfulPostCalls = new ArrayList<>(); } @@ -416,7 +416,7 @@ public static void postTaskCollection() { message.append("\r\n").append(failedMessages.size()).append(" resources failed to post."); - System.out.println(message); + logger.info(message.toString()); writeFailedPostAttemptsToLog(failedMessages); } @@ -439,9 +439,9 @@ private static void writeFailedPostAttemptsToLog(List failedMessages) { for (String str : failedMessages) { writer.write(str + "\n"); } - System.out.println("\r\nRecorded failed POST tasks to log file: " + new File(httpFailLogFilename).getAbsolutePath() + "\r\n"); + logger.info("\r\nRecorded failed POST tasks to log file: " + new File(httpFailLogFilename).getAbsolutePath() + "\r\n"); } catch (IOException e) { - System.out.println("\r\nRecording of failed POST tasks to log failed with exception: " + e.getMessage() + "\r\n"); + logger.info("\r\nRecording of failed POST tasks to log failed with exception: " + e.getMessage() + "\r\n"); } } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java index 72344d0de..96f61b851 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/IOUtils.java @@ -834,7 +834,7 @@ private static void setupMeasurePaths(FhirContext fhirContext) { resources.put(path, resource); } catch (Exception e) { if(path.toLowerCase().contains("measure")) { - System.out.println("Error reading in Measure from path: " + path + "\n" + e); + logger.error("Error reading in Measure from path: " + path, e); } } } diff --git a/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTest.json b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTest.json new file mode 100644 index 000000000..77600b865 --- /dev/null +++ b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTest.json @@ -0,0 +1,119 @@ +{ + "resourceType": "Library", + "id": "LibraryEvaluationTest", + "url": "http://mycontentig.com/fhir/Library/LibraryEvaluationTest", + "version": "1.0.000", + "name": "LibraryEvaluationTest", + "subjectCodeableConcept": { + "coding": [ { + "system": "http://hl7.org/fhir/resource-types", + "code": "Location" + } ] + }, + "relatedArtifact": [ { + "type": "depends-on", + "display": "FHIR model information", + "resource": "http://fhir.org/guides/cqf/common/Library/FHIR-ModelInfo|4.0.1" + }, { + "type": "depends-on", + "display": "Library Concepts", + "resource": "http://mycontentig.com/fhir/Library/LibraryEvaluationTestConcepts|1.0.000" + }, { + "type": "depends-on", + "display": "Library LET2", + "resource": "http://mycontentig.com/fhir/Library/LibraryEvaluationTestDependency|1.0.000" + }, { + "type": "depends-on", + "display": "Value set Problem List Condition Category", + "resource": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category" + } ], + "parameter": [ { + "name": "Patient", + "use": "out", + "min": 0, + "max": "1", + "type": "Patient" + }, { + "name": "Has Bone Narrowing Conditions", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Has Osteonecrosis Knee Conditions", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Has Angular Deformity Knee Conditions", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Has Presence of significant radiographic findings, which may include knee joint destruction, angular deformity, or severe narrowing", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Has Failure of Previous Proximal Tibial or Distal Femoral Osteotomy", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + }, { + "name": "Encounters from Dependency Library", + "use": "out", + "min": 0, + "max": "*", + "type": "Encounter" + }, { + "name": "Has Encounters", + "use": "out", + "min": 0, + "max": "1", + "type": "boolean" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Patient" ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Condition" ], + "mustSupport": [ "category" ], + "codeFilter": [ { + "path": "category", + "valueSet": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category" + } ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Condition" ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Condition" ] + }, { + "type": "Procedure", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Procedure" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ] + } ], + "content": [ { + "contentType": "text/cql", + "data": "bGlicmFyeSBMaWJyYXJ5RXZhbHVhdGlvblRlc3QgdmVyc2lvbiAnMS4wLjAwMCcKCnVzaW5nIEZISVIgdmVyc2lvbiAnNC4wLjEnCgppbmNsdWRlIExpYnJhcnlFdmFsdWF0aW9uVGVzdENvbmNlcHRzIHZlcnNpb24gJzEuMC4wMDAnIGNhbGxlZCBDb25jZXB0cwppbmNsdWRlIExpYnJhcnlFdmFsdWF0aW9uVGVzdERlcGVuZGVuY3kgdmVyc2lvbiAnMS4wLjAwMCcgY2FsbGVkIExFVDIKCmNvbnRleHQgUGF0aWVudAoKLy8gVGhpcyBleHByZXNzaW9uIGRvZXMgbm90IGRpcmVjdGx5IHJldHJpZXZlIGRhdGEgYW5kIHNvIGl0IGlzIG5vdCBuZWNlc3NhcnkgZm9yCi8vIERhdGFSZXF1aXJlbWVudCBpZGVudGlmaWNhdGlvbi4gSXQgaXMgY29tcG9zZWQgdGhpcyB3YXMgaW4gdGhlIGluZGljYXRpb25zIHRob3VnaAovLyBhbmQgc28gaXQgc2VlbXMgdGhhdCBpdCBfaXNfIHRoZSAicXVlc3Rpb24iIGFzIG9wcG9zZWQgdG8gdGhlIHVuZGVybHlpbmcgcmV0cmlldmFscwovLyBiZWluZy4KLyogN0FFQjMyRDdCRDhFNTJDNy1GMUNGQzExNTc5NjJDMUYzLTVEQjBEMERBNTM3OTA4RTUgKi8KZGVmaW5lICJIYXMgUHJlc2VuY2Ugb2Ygc2lnbmlmaWNhbnQgcmFkaW9ncmFwaGljIGZpbmRpbmdzLCB3aGljaCBtYXkgaW5jbHVkZSBrbmVlIGpvaW50IGRlc3RydWN0aW9uLCBhbmd1bGFyIGRlZm9ybWl0eSwgb3Igc2V2ZXJlIG5hcnJvd2luZyI6CiAgIkhhcyBCb25lIE5hcnJvd2luZyBDb25kaXRpb25zIgogICAgb3IgIkhhcyBPc3Rlb25lY3Jvc2lzIEtuZWUgQ29uZGl0aW9ucyIKICAgIG9yICJIYXMgQW5ndWxhciBEZWZvcm1pdHkgS25lZSBDb25kaXRpb25zIgoKZGVmaW5lICJIYXMgQm9uZSBOYXJyb3dpbmcgQ29uZGl0aW9ucyI6CiAgZXhpc3RzIChbQ29uZGl0aW9uOiBjYXRlZ29yeSBpbiBDb25jZXB0cy4iUHJvYmxlbSBMaXN0IENvbmRpdGlvbiBDYXRlZ29yeSJdKQoKZGVmaW5lICJIYXMgT3N0ZW9uZWNyb3NpcyBLbmVlIENvbmRpdGlvbnMiOgogIGV4aXN0cyAoW0NvbmRpdGlvbl0pCgpkZWZpbmUgIkhhcyBBbmd1bGFyIERlZm9ybWl0eSBLbmVlIENvbmRpdGlvbnMiOgogIGV4aXN0cyAoW0NvbmRpdGlvbl0pCgovKiAiUGF0aElkIjogIjdBRUIzMkQ3QkQ4RTUyQzctRDlFOTEwNEFCRDQ4QjNFRCIgKi8KZGVmaW5lICJIYXMgRmFpbHVyZSBvZiBQcmV2aW91cyBQcm94aW1hbCBUaWJpYWwgb3IgRGlzdGFsIEZlbW9yYWwgT3N0ZW90b215IjoKICBleGlzdHMgKFtQcm9jZWR1cmVdKQoKZGVmaW5lICJFbmNvdW50ZXJzIGZyb20gRGVwZW5kZW5jeSBMaWJyYXJ5IjoKICBMRVQyLiJFbmNvdW50ZXJzIgoKZGVmaW5lICJIYXMgRW5jb3VudGVycyI6CiAgICBleGlzdHMgKCJFbmNvdW50ZXJzIGZyb20gRGVwZW5kZW5jeSBMaWJyYXJ5IikK" + }, { + "contentType": "application/elm+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPGxpYnJhcnkgeG1sbnM9InVybjpobDctb3JnOmVsbTpyMSIgeG1sbnM6dD0idXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpmaGlyPSJodHRwOi8vaGw3Lm9yZy9maGlyIiB4bWxuczpxZG00Mz0idXJuOmhlYWx0aGl0LWdvdjpxZG06djRfMyIgeG1sbnM6cWRtNTM9InVybjpoZWFsdGhpdC1nb3Y6cWRtOnY1XzMiIHhtbG5zOmE9InVybjpobDctb3JnOmNxbC1hbm5vdGF0aW9uczpyMSI+CiAgIDxhbm5vdGF0aW9uIHRyYW5zbGF0b3JWZXJzaW9uPSIxLjQiIHRyYW5zbGF0b3JPcHRpb25zPSJFbmFibGVMb2NhdG9ycyxEaXNhYmxlTGlzdERlbW90aW9uLERpc2FibGVMaXN0UHJvbW90aW9uIiB4c2k6dHlwZT0iYTpDcWxUb0VsbUluZm8iLz4KICAgPGFubm90YXRpb24gbGlicmFyeVN5c3RlbT0iaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyIiBsaWJyYXJ5SWQ9IkxpYnJhcnlFdmFsdWF0aW9uVGVzdCIgbGlicmFyeVZlcnNpb249IjEuMC4wMDAiIHN0YXJ0TGluZT0iMjEiIHN0YXJ0Q2hhcj0iMTEiIGVuZExpbmU9IjIxIiBlbmRDaGFyPSI3NyIgbWVzc2FnZT0iQ291bGQgbm90IHJlc29sdmUgbWVtYmVyc2hpcCBvcGVyYXRvciBmb3IgdGVybWlub2xvZ3kgdGFyZ2V0IG9mIHRoZSByZXRyaWV2ZS4iIGVycm9yVHlwZT0ic2VtYW50aWMiIGVycm9yU2V2ZXJpdHk9Indhcm5pbmciIHhzaTp0eXBlPSJhOkNxbFRvRWxtRXJyb3IiLz4KICAgPGlkZW50aWZpZXIgaWQ9IkxpYnJhcnlFdmFsdWF0aW9uVGVzdCIgc3lzdGVtPSJodHRwOi8vbXljb250ZW50aWcuY29tL2ZoaXIiIHZlcnNpb249IjEuMC4wMDAiLz4KICAgPHNjaGVtYUlkZW50aWZpZXIgaWQ9InVybjpobDctb3JnOmVsbSIgdmVyc2lvbj0icjEiLz4KICAgPHVzaW5ncz4KICAgICAgPGRlZiBsb2NhbElkZW50aWZpZXI9IlN5c3RlbSIgdXJpPSJ1cm46aGw3LW9yZzplbG0tdHlwZXM6cjEiLz4KICAgICAgPGRlZiBsb2NhdG9yPSIzOjEtMzoyNiIgbG9jYWxJZGVudGlmaWVyPSJGSElSIiB1cmk9Imh0dHA6Ly9obDcub3JnL2ZoaXIiIHZlcnNpb249IjQuMC4xIi8+CiAgIDwvdXNpbmdzPgogICA8aW5jbHVkZXM+CiAgICAgIDxkZWYgbG9jYXRvcj0iNToxLTU6NzEiIGxvY2FsSWRlbnRpZmllcj0iQ29uY2VwdHMiIHBhdGg9Imh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpci9MaWJyYXJ5RXZhbHVhdGlvblRlc3RDb25jZXB0cyIgdmVyc2lvbj0iMS4wLjAwMCIvPgogICAgICA8ZGVmIGxvY2F0b3I9IjY6MS02OjY5IiBsb2NhbElkZW50aWZpZXI9IkxFVDIiIHBhdGg9Imh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpci9MaWJyYXJ5RXZhbHVhdGlvblRlc3REZXBlbmRlbmN5IiB2ZXJzaW9uPSIxLjAuMDAwIi8+CiAgIDwvaW5jbHVkZXM+CiAgIDxjb250ZXh0cz4KICAgICAgPGRlZiBsb2NhdG9yPSI4OjEtODoxNSIgbmFtZT0iUGF0aWVudCIvPgogICA8L2NvbnRleHRzPgogICA8c3RhdGVtZW50cz4KICAgICAgPGRlZiBsb2NhdG9yPSI4OjEtODoxNSIgbmFtZT0iUGF0aWVudCIgY29udGV4dD0iUGF0aWVudCI+CiAgICAgICAgIDxleHByZXNzaW9uIHhzaTp0eXBlPSJTaW5nbGV0b25Gcm9tIj4KICAgICAgICAgICAgPG9wZXJhbmQgbG9jYXRvcj0iODoxLTg6MTUiIGRhdGFUeXBlPSJmaGlyOlBhdGllbnQiIHRlbXBsYXRlSWQ9Imh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9QYXRpZW50IiB4c2k6dHlwZT0iUmV0cmlldmUiLz4KICAgICAgICAgPC9leHByZXNzaW9uPgogICAgICA8L2RlZj4KICAgICAgPGRlZiBsb2NhdG9yPSIyMDoxLTIxOjc4IiBuYW1lPSJIYXMgQm9uZSBOYXJyb3dpbmcgQ29uZGl0aW9ucyIgY29udGV4dD0iUGF0aWVudCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+CiAgICAgICAgIDxleHByZXNzaW9uIGxvY2F0b3I9IjIxOjMtMjE6NzgiIHhzaTp0eXBlPSJFeGlzdHMiPgogICAgICAgICAgICA8b3BlcmFuZCBsb2NhdG9yPSIyMToxMC0yMTo3OCIgZGF0YVR5cGU9ImZoaXI6Q29uZGl0aW9uIiB0ZW1wbGF0ZUlkPSJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vQ29uZGl0aW9uIiBjb2RlUHJvcGVydHk9ImNhdGVnb3J5IiBjb2RlQ29tcGFyYXRvcj0iaW4iIHhzaTp0eXBlPSJSZXRyaWV2ZSI+CiAgICAgICAgICAgICAgIDxjb2RlcyBsb2NhdG9yPSIyMTozNS0yMTo3NiIgbmFtZT0iUHJvYmxlbSBMaXN0IENvbmRpdGlvbiBDYXRlZ29yeSIgbGlicmFyeU5hbWU9IkNvbmNlcHRzIiB4c2k6dHlwZT0iVmFsdWVTZXRSZWYiLz4KICAgICAgICAgICAgPC9vcGVyYW5kPgogICAgICAgICA8L2V4cHJlc3Npb24+CiAgICAgIDwvZGVmPgogICAgICA8ZGVmIGxvY2F0b3I9IjIzOjEtMjQ6MjIiIG5hbWU9IkhhcyBPc3Rlb25lY3Jvc2lzIEtuZWUgQ29uZGl0aW9ucyIgY29udGV4dD0iUGF0aWVudCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+CiAgICAgICAgIDxleHByZXNzaW9uIGxvY2F0b3I9IjI0OjMtMjQ6MjIiIHhzaTp0eXBlPSJFeGlzdHMiPgogICAgICAgICAgICA8b3BlcmFuZCBsb2NhdG9yPSIyNDoxMC0yNDoyMiIgZGF0YVR5cGU9ImZoaXI6Q29uZGl0aW9uIiB0ZW1wbGF0ZUlkPSJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vQ29uZGl0aW9uIiB4c2k6dHlwZT0iUmV0cmlldmUiLz4KICAgICAgICAgPC9leHByZXNzaW9uPgogICAgICA8L2RlZj4KICAgICAgPGRlZiBsb2NhdG9yPSIyNjoxLTI3OjIyIiBuYW1lPSJIYXMgQW5ndWxhciBEZWZvcm1pdHkgS25lZSBDb25kaXRpb25zIiBjb250ZXh0PSJQYXRpZW50IiBhY2Nlc3NMZXZlbD0iUHVibGljIj4KICAgICAgICAgPGV4cHJlc3Npb24gbG9jYXRvcj0iMjc6My0yNzoyMiIgeHNpOnR5cGU9IkV4aXN0cyI+CiAgICAgICAgICAgIDxvcGVyYW5kIGxvY2F0b3I9IjI3OjEwLTI3OjIyIiBkYXRhVHlwZT0iZmhpcjpDb25kaXRpb24iIHRlbXBsYXRlSWQ9Imh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9Db25kaXRpb24iIHhzaTp0eXBlPSJSZXRyaWV2ZSIvPgogICAgICAgICA8L2V4cHJlc3Npb24+CiAgICAgIDwvZGVmPgogICAgICA8ZGVmIGxvY2F0b3I9IjE1OjEtMTg6NDYiIG5hbWU9IkhhcyBQcmVzZW5jZSBvZiBzaWduaWZpY2FudCByYWRpb2dyYXBoaWMgZmluZGluZ3MsIHdoaWNoIG1heSBpbmNsdWRlIGtuZWUgam9pbnQgZGVzdHJ1Y3Rpb24sIGFuZ3VsYXIgZGVmb3JtaXR5LCBvciBzZXZlcmUgbmFycm93aW5nIiBjb250ZXh0PSJQYXRpZW50IiBhY2Nlc3NMZXZlbD0iUHVibGljIj4KICAgICAgICAgPGV4cHJlc3Npb24gbG9jYXRvcj0iMTY6My0xODo0NiIgeHNpOnR5cGU9Ik9yIj4KICAgICAgICAgICAgPG9wZXJhbmQgbG9jYXRvcj0iMTY6My0xNzo0MiIgeHNpOnR5cGU9Ik9yIj4KICAgICAgICAgICAgICAgPG9wZXJhbmQgbG9jYXRvcj0iMTY6My0xNjozMyIgbmFtZT0iSGFzIEJvbmUgTmFycm93aW5nIENvbmRpdGlvbnMiIHhzaTp0eXBlPSJFeHByZXNzaW9uUmVmIi8+CiAgICAgICAgICAgICAgIDxvcGVyYW5kIGxvY2F0b3I9IjE3OjgtMTc6NDIiIG5hbWU9IkhhcyBPc3Rlb25lY3Jvc2lzIEtuZWUgQ29uZGl0aW9ucyIgeHNpOnR5cGU9IkV4cHJlc3Npb25SZWYiLz4KICAgICAgICAgICAgPC9vcGVyYW5kPgogICAgICAgICAgICA8b3BlcmFuZCBsb2NhdG9yPSIxODo4LTE4OjQ2IiBuYW1lPSJIYXMgQW5ndWxhciBEZWZvcm1pdHkgS25lZSBDb25kaXRpb25zIiB4c2k6dHlwZT0iRXhwcmVzc2lvblJlZiIvPgogICAgICAgICA8L2V4cHJlc3Npb24+CiAgICAgIDwvZGVmPgogICAgICA8ZGVmIGxvY2F0b3I9IjMwOjEtMzE6MjIiIG5hbWU9IkhhcyBGYWlsdXJlIG9mIFByZXZpb3VzIFByb3hpbWFsIFRpYmlhbCBvciBEaXN0YWwgRmVtb3JhbCBPc3Rlb3RvbXkiIGNvbnRleHQ9IlBhdGllbnQiIGFjY2Vzc0xldmVsPSJQdWJsaWMiPgogICAgICAgICA8ZXhwcmVzc2lvbiBsb2NhdG9yPSIzMTozLTMxOjIyIiB4c2k6dHlwZT0iRXhpc3RzIj4KICAgICAgICAgICAgPG9wZXJhbmQgbG9jYXRvcj0iMzE6MTAtMzE6MjIiIGRhdGFUeXBlPSJmaGlyOlByb2NlZHVyZSIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL1Byb2NlZHVyZSIgeHNpOnR5cGU9IlJldHJpZXZlIi8+CiAgICAgICAgIDwvZXhwcmVzc2lvbj4KICAgICAgPC9kZWY+CiAgICAgIDxkZWYgbG9jYXRvcj0iMzM6MS0zNDoxOSIgbmFtZT0iRW5jb3VudGVycyBmcm9tIERlcGVuZGVuY3kgTGlicmFyeSIgY29udGV4dD0iUGF0aWVudCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+CiAgICAgICAgIDxleHByZXNzaW9uIGxvY2F0b3I9IjM0OjMtMzQ6MTkiIG5hbWU9IkVuY291bnRlcnMiIGxpYnJhcnlOYW1lPSJMRVQyIiB4c2k6dHlwZT0iRXhwcmVzc2lvblJlZiIvPgogICAgICA8L2RlZj4KICAgICAgPGRlZiBsb2NhdG9yPSIzNjoxLTM3OjQ5IiBuYW1lPSJIYXMgRW5jb3VudGVycyIgY29udGV4dD0iUGF0aWVudCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+CiAgICAgICAgIDxleHByZXNzaW9uIGxvY2F0b3I9IjM3OjUtMzc6NDkiIHhzaTp0eXBlPSJFeGlzdHMiPgogICAgICAgICAgICA8b3BlcmFuZCBsb2NhdG9yPSIzNzoxMi0zNzo0OSIgbmFtZT0iRW5jb3VudGVycyBmcm9tIERlcGVuZGVuY3kgTGlicmFyeSIgeHNpOnR5cGU9IkV4cHJlc3Npb25SZWYiLz4KICAgICAgICAgPC9leHByZXNzaW9uPgogICAgICA8L2RlZj4KICAgPC9zdGF0ZW1lbnRzPgo8L2xpYnJhcnk+Cg==" + }, { + "contentType": "application/elm+json", + "data": "ewogICAibGlicmFyeSIgOiB7CiAgICAgICJhbm5vdGF0aW9uIiA6IFsgewogICAgICAgICAidHJhbnNsYXRvclZlcnNpb24iIDogIjEuNCIsCiAgICAgICAgICJ0cmFuc2xhdG9yT3B0aW9ucyIgOiAiRW5hYmxlTG9jYXRvcnMsRGlzYWJsZUxpc3REZW1vdGlvbixEaXNhYmxlTGlzdFByb21vdGlvbiIsCiAgICAgICAgICJ0eXBlIiA6ICJDcWxUb0VsbUluZm8iCiAgICAgIH0sIHsKICAgICAgICAgImxpYnJhcnlTeXN0ZW0iIDogImh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpciIsCiAgICAgICAgICJsaWJyYXJ5SWQiIDogIkxpYnJhcnlFdmFsdWF0aW9uVGVzdCIsCiAgICAgICAgICJsaWJyYXJ5VmVyc2lvbiIgOiAiMS4wLjAwMCIsCiAgICAgICAgICJzdGFydExpbmUiIDogMjEsCiAgICAgICAgICJzdGFydENoYXIiIDogMTEsCiAgICAgICAgICJlbmRMaW5lIiA6IDIxLAogICAgICAgICAiZW5kQ2hhciIgOiA3NywKICAgICAgICAgIm1lc3NhZ2UiIDogIkNvdWxkIG5vdCByZXNvbHZlIG1lbWJlcnNoaXAgb3BlcmF0b3IgZm9yIHRlcm1pbm9sb2d5IHRhcmdldCBvZiB0aGUgcmV0cmlldmUuIiwKICAgICAgICAgImVycm9yVHlwZSIgOiAic2VtYW50aWMiLAogICAgICAgICAiZXJyb3JTZXZlcml0eSIgOiAid2FybmluZyIsCiAgICAgICAgICJ0eXBlIiA6ICJDcWxUb0VsbUVycm9yIgogICAgICB9IF0sCiAgICAgICJpZGVudGlmaWVyIiA6IHsKICAgICAgICAgImlkIiA6ICJMaWJyYXJ5RXZhbHVhdGlvblRlc3QiLAogICAgICAgICAic3lzdGVtIiA6ICJodHRwOi8vbXljb250ZW50aWcuY29tL2ZoaXIiLAogICAgICAgICAidmVyc2lvbiIgOiAiMS4wLjAwMCIKICAgICAgfSwKICAgICAgInNjaGVtYUlkZW50aWZpZXIiIDogewogICAgICAgICAiaWQiIDogInVybjpobDctb3JnOmVsbSIsCiAgICAgICAgICJ2ZXJzaW9uIiA6ICJyMSIKICAgICAgfSwKICAgICAgInVzaW5ncyIgOiB7CiAgICAgICAgICJkZWYiIDogWyB7CiAgICAgICAgICAgICJsb2NhbElkZW50aWZpZXIiIDogIlN5c3RlbSIsCiAgICAgICAgICAgICJ1cmkiIDogInVybjpobDctb3JnOmVsbS10eXBlczpyMSIKICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiMzoxLTM6MjYiLAogICAgICAgICAgICAibG9jYWxJZGVudGlmaWVyIiA6ICJGSElSIiwKICAgICAgICAgICAgInVyaSIgOiAiaHR0cDovL2hsNy5vcmcvZmhpciIsCiAgICAgICAgICAgICJ2ZXJzaW9uIiA6ICI0LjAuMSIKICAgICAgICAgfSBdCiAgICAgIH0sCiAgICAgICJpbmNsdWRlcyIgOiB7CiAgICAgICAgICJkZWYiIDogWyB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICI1OjEtNTo3MSIsCiAgICAgICAgICAgICJsb2NhbElkZW50aWZpZXIiIDogIkNvbmNlcHRzIiwKICAgICAgICAgICAgInBhdGgiIDogImh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpci9MaWJyYXJ5RXZhbHVhdGlvblRlc3RDb25jZXB0cyIsCiAgICAgICAgICAgICJ2ZXJzaW9uIiA6ICIxLjAuMDAwIgogICAgICAgICB9LCB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICI2OjEtNjo2OSIsCiAgICAgICAgICAgICJsb2NhbElkZW50aWZpZXIiIDogIkxFVDIiLAogICAgICAgICAgICAicGF0aCIgOiAiaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyL0xpYnJhcnlFdmFsdWF0aW9uVGVzdERlcGVuZGVuY3kiLAogICAgICAgICAgICAidmVyc2lvbiIgOiAiMS4wLjAwMCIKICAgICAgICAgfSBdCiAgICAgIH0sCiAgICAgICJjb250ZXh0cyIgOiB7CiAgICAgICAgICJkZWYiIDogWyB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICI4OjEtODoxNSIsCiAgICAgICAgICAgICJuYW1lIiA6ICJQYXRpZW50IgogICAgICAgICB9IF0KICAgICAgfSwKICAgICAgInN0YXRlbWVudHMiIDogewogICAgICAgICAiZGVmIiA6IFsgewogICAgICAgICAgICAibG9jYXRvciIgOiAiODoxLTg6MTUiLAogICAgICAgICAgICAibmFtZSIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJjb250ZXh0IiA6ICJQYXRpZW50IiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAidHlwZSIgOiAiU2luZ2xldG9uRnJvbSIsCiAgICAgICAgICAgICAgICJvcGVyYW5kIiA6IHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjg6MS04OjE1IiwKICAgICAgICAgICAgICAgICAgImRhdGFUeXBlIiA6ICJ7aHR0cDovL2hsNy5vcmcvZmhpcn1QYXRpZW50IiwKICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9QYXRpZW50IiwKICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlJldHJpZXZlIgogICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiMjA6MS0yMTo3OCIsCiAgICAgICAgICAgICJuYW1lIiA6ICJIYXMgQm9uZSBOYXJyb3dpbmcgQ29uZGl0aW9ucyIsCiAgICAgICAgICAgICJjb250ZXh0IiA6ICJQYXRpZW50IiwKICAgICAgICAgICAgImFjY2Vzc0xldmVsIiA6ICJQdWJsaWMiLAogICAgICAgICAgICAiZXhwcmVzc2lvbiIgOiB7CiAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyMTozLTIxOjc4IiwKICAgICAgICAgICAgICAgInR5cGUiIDogIkV4aXN0cyIsCiAgICAgICAgICAgICAgICJvcGVyYW5kIiA6IHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjIxOjEwLTIxOjc4IiwKICAgICAgICAgICAgICAgICAgImRhdGFUeXBlIiA6ICJ7aHR0cDovL2hsNy5vcmcvZmhpcn1Db25kaXRpb24iLAogICAgICAgICAgICAgICAgICAidGVtcGxhdGVJZCIgOiAiaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0NvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICAgICJjb2RlUHJvcGVydHkiIDogImNhdGVnb3J5IiwKICAgICAgICAgICAgICAgICAgImNvZGVDb21wYXJhdG9yIiA6ICJpbiIsCiAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJSZXRyaWV2ZSIsCiAgICAgICAgICAgICAgICAgICJjb2RlcyIgOiB7CiAgICAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyMTozNS0yMTo3NiIsCiAgICAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJQcm9ibGVtIExpc3QgQ29uZGl0aW9uIENhdGVnb3J5IiwKICAgICAgICAgICAgICAgICAgICAgImxpYnJhcnlOYW1lIiA6ICJDb25jZXB0cyIsCiAgICAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJWYWx1ZVNldFJlZiIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiMjM6MS0yNDoyMiIsCiAgICAgICAgICAgICJuYW1lIiA6ICJIYXMgT3N0ZW9uZWNyb3NpcyBLbmVlIENvbmRpdGlvbnMiLAogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjQ6My0yNDoyMiIsCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJFeGlzdHMiLAogICAgICAgICAgICAgICAib3BlcmFuZCIgOiB7CiAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyNDoxMC0yNDoyMiIsCiAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9Q29uZGl0aW9uIiwKICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9Db25kaXRpb24iLAogICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiCiAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICB9LCB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyNjoxLTI3OjIyIiwKICAgICAgICAgICAgIm5hbWUiIDogIkhhcyBBbmd1bGFyIERlZm9ybWl0eSBLbmVlIENvbmRpdGlvbnMiLAogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMjc6My0yNzoyMiIsCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJFeGlzdHMiLAogICAgICAgICAgICAgICAib3BlcmFuZCIgOiB7CiAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIyNzoxMC0yNzoyMiIsCiAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9Q29uZGl0aW9uIiwKICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9Db25kaXRpb24iLAogICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiCiAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICB9LCB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIxNToxLTE4OjQ2IiwKICAgICAgICAgICAgIm5hbWUiIDogIkhhcyBQcmVzZW5jZSBvZiBzaWduaWZpY2FudCByYWRpb2dyYXBoaWMgZmluZGluZ3MsIHdoaWNoIG1heSBpbmNsdWRlIGtuZWUgam9pbnQgZGVzdHJ1Y3Rpb24sIGFuZ3VsYXIgZGVmb3JtaXR5LCBvciBzZXZlcmUgbmFycm93aW5nIiwKICAgICAgICAgICAgImNvbnRleHQiIDogIlBhdGllbnQiLAogICAgICAgICAgICAiYWNjZXNzTGV2ZWwiIDogIlB1YmxpYyIsCiAgICAgICAgICAgICJleHByZXNzaW9uIiA6IHsKICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjE2OjMtMTg6NDYiLAogICAgICAgICAgICAgICAidHlwZSIgOiAiT3IiLAogICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjE2OjMtMTc6NDIiLAogICAgICAgICAgICAgICAgICAidHlwZSIgOiAiT3IiLAogICAgICAgICAgICAgICAgICAib3BlcmFuZCIgOiBbIHsKICAgICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjE2OjMtMTY6MzMiLAogICAgICAgICAgICAgICAgICAgICAibmFtZSIgOiAiSGFzIEJvbmUgTmFycm93aW5nIENvbmRpdGlvbnMiLAogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiRXhwcmVzc2lvblJlZiIKICAgICAgICAgICAgICAgICAgfSwgewogICAgICAgICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMTc6OC0xNzo0MiIsCiAgICAgICAgICAgICAgICAgICAgICJuYW1lIiA6ICJIYXMgT3N0ZW9uZWNyb3NpcyBLbmVlIENvbmRpdGlvbnMiLAogICAgICAgICAgICAgICAgICAgICAidHlwZSIgOiAiRXhwcmVzc2lvblJlZiIKICAgICAgICAgICAgICAgICAgfSBdCiAgICAgICAgICAgICAgIH0sIHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjE4OjgtMTg6NDYiLAogICAgICAgICAgICAgICAgICAibmFtZSIgOiAiSGFzIEFuZ3VsYXIgRGVmb3JtaXR5IEtuZWUgQ29uZGl0aW9ucyIsCiAgICAgICAgICAgICAgICAgICJ0eXBlIiA6ICJFeHByZXNzaW9uUmVmIgogICAgICAgICAgICAgICB9IF0KICAgICAgICAgICAgfQogICAgICAgICB9LCB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMDoxLTMxOjIyIiwKICAgICAgICAgICAgIm5hbWUiIDogIkhhcyBGYWlsdXJlIG9mIFByZXZpb3VzIFByb3hpbWFsIFRpYmlhbCBvciBEaXN0YWwgRmVtb3JhbCBPc3Rlb3RvbXkiLAogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzE6My0zMToyMiIsCiAgICAgICAgICAgICAgICJ0eXBlIiA6ICJFeGlzdHMiLAogICAgICAgICAgICAgICAib3BlcmFuZCIgOiB7CiAgICAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMToxMC0zMToyMiIsCiAgICAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9UHJvY2VkdXJlIiwKICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9Qcm9jZWR1cmUiLAogICAgICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiCiAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgICB9LCB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzMzoxLTM0OjE5IiwKICAgICAgICAgICAgIm5hbWUiIDogIkVuY291bnRlcnMgZnJvbSBEZXBlbmRlbmN5IExpYnJhcnkiLAogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMzQ6My0zNDoxOSIsCiAgICAgICAgICAgICAgICJuYW1lIiA6ICJFbmNvdW50ZXJzIiwKICAgICAgICAgICAgICAgImxpYnJhcnlOYW1lIiA6ICJMRVQyIiwKICAgICAgICAgICAgICAgInR5cGUiIDogIkV4cHJlc3Npb25SZWYiCiAgICAgICAgICAgIH0KICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiMzY6MS0zNzo0OSIsCiAgICAgICAgICAgICJuYW1lIiA6ICJIYXMgRW5jb3VudGVycyIsCiAgICAgICAgICAgICJjb250ZXh0IiA6ICJQYXRpZW50IiwKICAgICAgICAgICAgImFjY2Vzc0xldmVsIiA6ICJQdWJsaWMiLAogICAgICAgICAgICAiZXhwcmVzc2lvbiIgOiB7CiAgICAgICAgICAgICAgICJsb2NhdG9yIiA6ICIzNzo1LTM3OjQ5IiwKICAgICAgICAgICAgICAgInR5cGUiIDogIkV4aXN0cyIsCiAgICAgICAgICAgICAgICJvcGVyYW5kIiA6IHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjM3OjEyLTM3OjQ5IiwKICAgICAgICAgICAgICAgICAgIm5hbWUiIDogIkVuY291bnRlcnMgZnJvbSBEZXBlbmRlbmN5IExpYnJhcnkiLAogICAgICAgICAgICAgICAgICAidHlwZSIgOiAiRXhwcmVzc2lvblJlZiIKICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgIH0gXQogICAgICB9CiAgIH0KfQ==" + } ] +} \ No newline at end of file diff --git a/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestConcepts.json b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestConcepts.json new file mode 100644 index 000000000..1c74efc48 --- /dev/null +++ b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestConcepts.json @@ -0,0 +1,26 @@ +{ + "resourceType": "Library", + "id": "LibraryEvaluationTestConcepts", + "url": "http://mycontentig.com/fhir/Library/LibraryEvaluationTestConcepts", + "version": "1.0.000", + "name": "LibraryEvaluationTestConcepts", + "relatedArtifact": [ { + "type": "depends-on", + "display": "FHIR model information", + "resource": "http://fhir.org/guides/cqf/common/Library/FHIR-ModelInfo|4.0.1" + }, { + "type": "depends-on", + "display": "Value set Problem List Condition Category", + "resource": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category" + } ], + "content": [ { + "contentType": "text/cql", + "data": "bGlicmFyeSBMaWJyYXJ5RXZhbHVhdGlvblRlc3RDb25jZXB0cyB2ZXJzaW9uICcxLjAuMDAwJwoKdXNpbmcgRkhJUiB2ZXJzaW9uICc0LjAuMScKCnZhbHVlc2V0ICJQcm9ibGVtIExpc3QgQ29uZGl0aW9uIENhdGVnb3J5IjogJ2h0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpci9WYWx1ZVNldC9jb25kaXRpb24tcHJvYmxlbS1saXN0LWNhdGVnb3J5Jwo=" + }, { + "contentType": "application/elm+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPGxpYnJhcnkgeG1sbnM9InVybjpobDctb3JnOmVsbTpyMSIgeG1sbnM6dD0idXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpmaGlyPSJodHRwOi8vaGw3Lm9yZy9maGlyIiB4bWxuczpxZG00Mz0idXJuOmhlYWx0aGl0LWdvdjpxZG06djRfMyIgeG1sbnM6cWRtNTM9InVybjpoZWFsdGhpdC1nb3Y6cWRtOnY1XzMiIHhtbG5zOmE9InVybjpobDctb3JnOmNxbC1hbm5vdGF0aW9uczpyMSI+CiAgIDxhbm5vdGF0aW9uIHRyYW5zbGF0b3JWZXJzaW9uPSIxLjQiIHRyYW5zbGF0b3JPcHRpb25zPSJFbmFibGVMb2NhdG9ycyxEaXNhYmxlTGlzdERlbW90aW9uLERpc2FibGVMaXN0UHJvbW90aW9uIiB4c2k6dHlwZT0iYTpDcWxUb0VsbUluZm8iLz4KICAgPGlkZW50aWZpZXIgaWQ9IkxpYnJhcnlFdmFsdWF0aW9uVGVzdENvbmNlcHRzIiBzeXN0ZW09Imh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpciIgdmVyc2lvbj0iMS4wLjAwMCIvPgogICA8c2NoZW1hSWRlbnRpZmllciBpZD0idXJuOmhsNy1vcmc6ZWxtIiB2ZXJzaW9uPSJyMSIvPgogICA8dXNpbmdzPgogICAgICA8ZGVmIGxvY2FsSWRlbnRpZmllcj0iU3lzdGVtIiB1cmk9InVybjpobDctb3JnOmVsbS10eXBlczpyMSIvPgogICAgICA8ZGVmIGxvY2F0b3I9IjM6MS0zOjI2IiBsb2NhbElkZW50aWZpZXI9IkZISVIiIHVyaT0iaHR0cDovL2hsNy5vcmcvZmhpciIgdmVyc2lvbj0iNC4wLjEiLz4KICAgPC91c2luZ3M+CiAgIDx2YWx1ZVNldHM+CiAgICAgIDxkZWYgbG9jYXRvcj0iNToxLTU6MTE0IiBuYW1lPSJQcm9ibGVtIExpc3QgQ29uZGl0aW9uIENhdGVnb3J5IiBpZD0iaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyL1ZhbHVlU2V0L2NvbmRpdGlvbi1wcm9ibGVtLWxpc3QtY2F0ZWdvcnkiIGFjY2Vzc0xldmVsPSJQdWJsaWMiLz4KICAgPC92YWx1ZVNldHM+CjwvbGlicmFyeT4K" + }, { + "contentType": "application/elm+json", + "data": "ewogICAibGlicmFyeSIgOiB7CiAgICAgICJhbm5vdGF0aW9uIiA6IFsgewogICAgICAgICAidHJhbnNsYXRvclZlcnNpb24iIDogIjEuNCIsCiAgICAgICAgICJ0cmFuc2xhdG9yT3B0aW9ucyIgOiAiRW5hYmxlTG9jYXRvcnMsRGlzYWJsZUxpc3REZW1vdGlvbixEaXNhYmxlTGlzdFByb21vdGlvbiIsCiAgICAgICAgICJ0eXBlIiA6ICJDcWxUb0VsbUluZm8iCiAgICAgIH0gXSwKICAgICAgImlkZW50aWZpZXIiIDogewogICAgICAgICAiaWQiIDogIkxpYnJhcnlFdmFsdWF0aW9uVGVzdENvbmNlcHRzIiwKICAgICAgICAgInN5c3RlbSIgOiAiaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyIiwKICAgICAgICAgInZlcnNpb24iIDogIjEuMC4wMDAiCiAgICAgIH0sCiAgICAgICJzY2hlbWFJZGVudGlmaWVyIiA6IHsKICAgICAgICAgImlkIiA6ICJ1cm46aGw3LW9yZzplbG0iLAogICAgICAgICAidmVyc2lvbiIgOiAicjEiCiAgICAgIH0sCiAgICAgICJ1c2luZ3MiIDogewogICAgICAgICAiZGVmIiA6IFsgewogICAgICAgICAgICAibG9jYWxJZGVudGlmaWVyIiA6ICJTeXN0ZW0iLAogICAgICAgICAgICAidXJpIiA6ICJ1cm46aGw3LW9yZzplbG0tdHlwZXM6cjEiCiAgICAgICAgIH0sIHsKICAgICAgICAgICAgImxvY2F0b3IiIDogIjM6MS0zOjI2IiwKICAgICAgICAgICAgImxvY2FsSWRlbnRpZmllciIgOiAiRkhJUiIsCiAgICAgICAgICAgICJ1cmkiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIiLAogICAgICAgICAgICAidmVyc2lvbiIgOiAiNC4wLjEiCiAgICAgICAgIH0gXQogICAgICB9LAogICAgICAidmFsdWVTZXRzIiA6IHsKICAgICAgICAgImRlZiIgOiBbIHsKICAgICAgICAgICAgImxvY2F0b3IiIDogIjU6MS01OjExNCIsCiAgICAgICAgICAgICJuYW1lIiA6ICJQcm9ibGVtIExpc3QgQ29uZGl0aW9uIENhdGVnb3J5IiwKICAgICAgICAgICAgImlkIiA6ICJodHRwOi8vbXljb250ZW50aWcuY29tL2ZoaXIvVmFsdWVTZXQvY29uZGl0aW9uLXByb2JsZW0tbGlzdC1jYXRlZ29yeSIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIgogICAgICAgICB9IF0KICAgICAgfQogICB9Cn0=" + } ] +} \ No newline at end of file diff --git a/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestDependency.json b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestDependency.json new file mode 100644 index 000000000..1b506028d --- /dev/null +++ b/tooling/tooling-cli/bundleResourcesResults/Library-LibraryEvaluationTestDependency.json @@ -0,0 +1,64 @@ +{ + "resourceType": "Library", + "id": "LibraryEvaluationTestDependency", + "url": "http://mycontentig.com/fhir/Library/LibraryEvaluationTestDependency", + "version": "1.0.000", + "name": "LibraryEvaluationTestDependency", + "relatedArtifact": [ { + "type": "depends-on", + "display": "FHIR model information", + "resource": "http://fhir.org/guides/cqf/common/Library/FHIR-ModelInfo|4.0.1" + }, { + "type": "depends-on", + "display": "Library CommonCx", + "resource": "http://mycontentig.com/fhir/Library/LibraryEvaluationTestConcepts|1.0.000" + }, { + "type": "depends-on", + "display": "Value set Problem List Condition Category", + "resource": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category" + } ], + "parameter": [ { + "name": "Patient", + "use": "out", + "min": 0, + "max": "1", + "type": "Patient" + }, { + "name": "Encounters", + "use": "out", + "min": 0, + "max": "*", + "type": "Encounter" + }, { + "name": "Bone Narrowing Conditions", + "use": "out", + "min": 0, + "max": "*", + "type": "Condition" + } ], + "dataRequirement": [ { + "type": "Patient", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Patient" ] + }, { + "type": "Encounter", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Encounter" ] + }, { + "type": "Condition", + "profile": [ "http://hl7.org/fhir/StructureDefinition/Condition" ], + "mustSupport": [ "category" ], + "codeFilter": [ { + "path": "category", + "valueSet": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category" + } ] + } ], + "content": [ { + "contentType": "text/cql", + "data": "bGlicmFyeSBMaWJyYXJ5RXZhbHVhdGlvblRlc3REZXBlbmRlbmN5IHZlcnNpb24gJzEuMC4wMDAnCgp1c2luZyBGSElSIHZlcnNpb24gJzQuMC4xJwoKaW5jbHVkZSBMaWJyYXJ5RXZhbHVhdGlvblRlc3RDb25jZXB0cyB2ZXJzaW9uICcxLjAuMDAwJyBjYWxsZWQgQ29tbW9uQ3gKCmNvbnRleHQgUGF0aWVudAoKZGVmaW5lICJFbmNvdW50ZXJzIjoKICBbRW5jb3VudGVyXQoKZGVmaW5lICJCb25lIE5hcnJvd2luZyBDb25kaXRpb25zIjoKICBbQ29uZGl0aW9uOiBjYXRlZ29yeSBpbiBDb21tb25DeC4iUHJvYmxlbSBMaXN0IENvbmRpdGlvbiBDYXRlZ29yeSJdCg==" + }, { + "contentType": "application/elm+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPGxpYnJhcnkgeG1sbnM9InVybjpobDctb3JnOmVsbTpyMSIgeG1sbnM6dD0idXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4c2Q9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczpmaGlyPSJodHRwOi8vaGw3Lm9yZy9maGlyIiB4bWxuczpxZG00Mz0idXJuOmhlYWx0aGl0LWdvdjpxZG06djRfMyIgeG1sbnM6cWRtNTM9InVybjpoZWFsdGhpdC1nb3Y6cWRtOnY1XzMiIHhtbG5zOmE9InVybjpobDctb3JnOmNxbC1hbm5vdGF0aW9uczpyMSI+CiAgIDxhbm5vdGF0aW9uIHRyYW5zbGF0b3JWZXJzaW9uPSIxLjQiIHRyYW5zbGF0b3JPcHRpb25zPSJFbmFibGVMb2NhdG9ycyxEaXNhYmxlTGlzdERlbW90aW9uLERpc2FibGVMaXN0UHJvbW90aW9uIiB4c2k6dHlwZT0iYTpDcWxUb0VsbUluZm8iLz4KICAgPGFubm90YXRpb24gbGlicmFyeVN5c3RlbT0iaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyIiBsaWJyYXJ5SWQ9IkxpYnJhcnlFdmFsdWF0aW9uVGVzdERlcGVuZGVuY3kiIGxpYnJhcnlWZXJzaW9uPSIxLjAuMDAwIiBzdGFydExpbmU9IjEzIiBzdGFydENoYXI9IjMiIGVuZExpbmU9IjEzIiBlbmRDaGFyPSI2OSIgbWVzc2FnZT0iQ291bGQgbm90IHJlc29sdmUgbWVtYmVyc2hpcCBvcGVyYXRvciBmb3IgdGVybWlub2xvZ3kgdGFyZ2V0IG9mIHRoZSByZXRyaWV2ZS4iIGVycm9yVHlwZT0ic2VtYW50aWMiIGVycm9yU2V2ZXJpdHk9Indhcm5pbmciIHhzaTp0eXBlPSJhOkNxbFRvRWxtRXJyb3IiLz4KICAgPGlkZW50aWZpZXIgaWQ9IkxpYnJhcnlFdmFsdWF0aW9uVGVzdERlcGVuZGVuY3kiIHN5c3RlbT0iaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyIiB2ZXJzaW9uPSIxLjAuMDAwIi8+CiAgIDxzY2hlbWFJZGVudGlmaWVyIGlkPSJ1cm46aGw3LW9yZzplbG0iIHZlcnNpb249InIxIi8+CiAgIDx1c2luZ3M+CiAgICAgIDxkZWYgbG9jYWxJZGVudGlmaWVyPSJTeXN0ZW0iIHVyaT0idXJuOmhsNy1vcmc6ZWxtLXR5cGVzOnIxIi8+CiAgICAgIDxkZWYgbG9jYXRvcj0iMzoxLTM6MjYiIGxvY2FsSWRlbnRpZmllcj0iRkhJUiIgdXJpPSJodHRwOi8vaGw3Lm9yZy9maGlyIiB2ZXJzaW9uPSI0LjAuMSIvPgogICA8L3VzaW5ncz4KICAgPGluY2x1ZGVzPgogICAgICA8ZGVmIGxvY2F0b3I9IjU6MS01OjcxIiBsb2NhbElkZW50aWZpZXI9IkNvbW1vbkN4IiBwYXRoPSJodHRwOi8vbXljb250ZW50aWcuY29tL2ZoaXIvTGlicmFyeUV2YWx1YXRpb25UZXN0Q29uY2VwdHMiIHZlcnNpb249IjEuMC4wMDAiLz4KICAgPC9pbmNsdWRlcz4KICAgPGNvbnRleHRzPgogICAgICA8ZGVmIGxvY2F0b3I9Ijc6MS03OjE1IiBuYW1lPSJQYXRpZW50Ii8+CiAgIDwvY29udGV4dHM+CiAgIDxzdGF0ZW1lbnRzPgogICAgICA8ZGVmIGxvY2F0b3I9Ijc6MS03OjE1IiBuYW1lPSJQYXRpZW50IiBjb250ZXh0PSJQYXRpZW50Ij4KICAgICAgICAgPGV4cHJlc3Npb24geHNpOnR5cGU9IlNpbmdsZXRvbkZyb20iPgogICAgICAgICAgICA8b3BlcmFuZCBsb2NhdG9yPSI3OjEtNzoxNSIgZGF0YVR5cGU9ImZoaXI6UGF0aWVudCIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL1BhdGllbnQiIHhzaTp0eXBlPSJSZXRyaWV2ZSIvPgogICAgICAgICA8L2V4cHJlc3Npb24+CiAgICAgIDwvZGVmPgogICAgICA8ZGVmIGxvY2F0b3I9Ijk6MS0xMDoxMyIgbmFtZT0iRW5jb3VudGVycyIgY29udGV4dD0iUGF0aWVudCIgYWNjZXNzTGV2ZWw9IlB1YmxpYyI+CiAgICAgICAgIDxleHByZXNzaW9uIGxvY2F0b3I9IjEwOjMtMTA6MTMiIGRhdGFUeXBlPSJmaGlyOkVuY291bnRlciIgdGVtcGxhdGVJZD0iaHR0cDovL2hsNy5vcmcvZmhpci9TdHJ1Y3R1cmVEZWZpbml0aW9uL0VuY291bnRlciIgeHNpOnR5cGU9IlJldHJpZXZlIi8+CiAgICAgIDwvZGVmPgogICAgICA8ZGVmIGxvY2F0b3I9IjEyOjEtMTM6NjkiIG5hbWU9IkJvbmUgTmFycm93aW5nIENvbmRpdGlvbnMiIGNvbnRleHQ9IlBhdGllbnQiIGFjY2Vzc0xldmVsPSJQdWJsaWMiPgogICAgICAgICA8ZXhwcmVzc2lvbiBsb2NhdG9yPSIxMzozLTEzOjY5IiBkYXRhVHlwZT0iZmhpcjpDb25kaXRpb24iIHRlbXBsYXRlSWQ9Imh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9Db25kaXRpb24iIGNvZGVQcm9wZXJ0eT0iY2F0ZWdvcnkiIGNvZGVDb21wYXJhdG9yPSJpbiIgeHNpOnR5cGU9IlJldHJpZXZlIj4KICAgICAgICAgICAgPGNvZGVzIGxvY2F0b3I9IjEzOjI3LTEzOjY4IiBuYW1lPSJQcm9ibGVtIExpc3QgQ29uZGl0aW9uIENhdGVnb3J5IiBsaWJyYXJ5TmFtZT0iQ29tbW9uQ3giIHhzaTp0eXBlPSJWYWx1ZVNldFJlZiIvPgogICAgICAgICA8L2V4cHJlc3Npb24+CiAgICAgIDwvZGVmPgogICA8L3N0YXRlbWVudHM+CjwvbGlicmFyeT4K" + }, { + "contentType": "application/elm+json", + "data": "ewogICAibGlicmFyeSIgOiB7CiAgICAgICJhbm5vdGF0aW9uIiA6IFsgewogICAgICAgICAidHJhbnNsYXRvclZlcnNpb24iIDogIjEuNCIsCiAgICAgICAgICJ0cmFuc2xhdG9yT3B0aW9ucyIgOiAiRW5hYmxlTG9jYXRvcnMsRGlzYWJsZUxpc3REZW1vdGlvbixEaXNhYmxlTGlzdFByb21vdGlvbiIsCiAgICAgICAgICJ0eXBlIiA6ICJDcWxUb0VsbUluZm8iCiAgICAgIH0sIHsKICAgICAgICAgImxpYnJhcnlTeXN0ZW0iIDogImh0dHA6Ly9teWNvbnRlbnRpZy5jb20vZmhpciIsCiAgICAgICAgICJsaWJyYXJ5SWQiIDogIkxpYnJhcnlFdmFsdWF0aW9uVGVzdERlcGVuZGVuY3kiLAogICAgICAgICAibGlicmFyeVZlcnNpb24iIDogIjEuMC4wMDAiLAogICAgICAgICAic3RhcnRMaW5lIiA6IDEzLAogICAgICAgICAic3RhcnRDaGFyIiA6IDMsCiAgICAgICAgICJlbmRMaW5lIiA6IDEzLAogICAgICAgICAiZW5kQ2hhciIgOiA2OSwKICAgICAgICAgIm1lc3NhZ2UiIDogIkNvdWxkIG5vdCByZXNvbHZlIG1lbWJlcnNoaXAgb3BlcmF0b3IgZm9yIHRlcm1pbm9sb2d5IHRhcmdldCBvZiB0aGUgcmV0cmlldmUuIiwKICAgICAgICAgImVycm9yVHlwZSIgOiAic2VtYW50aWMiLAogICAgICAgICAiZXJyb3JTZXZlcml0eSIgOiAid2FybmluZyIsCiAgICAgICAgICJ0eXBlIiA6ICJDcWxUb0VsbUVycm9yIgogICAgICB9IF0sCiAgICAgICJpZGVudGlmaWVyIiA6IHsKICAgICAgICAgImlkIiA6ICJMaWJyYXJ5RXZhbHVhdGlvblRlc3REZXBlbmRlbmN5IiwKICAgICAgICAgInN5c3RlbSIgOiAiaHR0cDovL215Y29udGVudGlnLmNvbS9maGlyIiwKICAgICAgICAgInZlcnNpb24iIDogIjEuMC4wMDAiCiAgICAgIH0sCiAgICAgICJzY2hlbWFJZGVudGlmaWVyIiA6IHsKICAgICAgICAgImlkIiA6ICJ1cm46aGw3LW9yZzplbG0iLAogICAgICAgICAidmVyc2lvbiIgOiAicjEiCiAgICAgIH0sCiAgICAgICJ1c2luZ3MiIDogewogICAgICAgICAiZGVmIiA6IFsgewogICAgICAgICAgICAibG9jYWxJZGVudGlmaWVyIiA6ICJTeXN0ZW0iLAogICAgICAgICAgICAidXJpIiA6ICJ1cm46aGw3LW9yZzplbG0tdHlwZXM6cjEiCiAgICAgICAgIH0sIHsKICAgICAgICAgICAgImxvY2F0b3IiIDogIjM6MS0zOjI2IiwKICAgICAgICAgICAgImxvY2FsSWRlbnRpZmllciIgOiAiRkhJUiIsCiAgICAgICAgICAgICJ1cmkiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIiLAogICAgICAgICAgICAidmVyc2lvbiIgOiAiNC4wLjEiCiAgICAgICAgIH0gXQogICAgICB9LAogICAgICAiaW5jbHVkZXMiIDogewogICAgICAgICAiZGVmIiA6IFsgewogICAgICAgICAgICAibG9jYXRvciIgOiAiNToxLTU6NzEiLAogICAgICAgICAgICAibG9jYWxJZGVudGlmaWVyIiA6ICJDb21tb25DeCIsCiAgICAgICAgICAgICJwYXRoIiA6ICJodHRwOi8vbXljb250ZW50aWcuY29tL2ZoaXIvTGlicmFyeUV2YWx1YXRpb25UZXN0Q29uY2VwdHMiLAogICAgICAgICAgICAidmVyc2lvbiIgOiAiMS4wLjAwMCIKICAgICAgICAgfSBdCiAgICAgIH0sCiAgICAgICJjb250ZXh0cyIgOiB7CiAgICAgICAgICJkZWYiIDogWyB7CiAgICAgICAgICAgICJsb2NhdG9yIiA6ICI3OjEtNzoxNSIsCiAgICAgICAgICAgICJuYW1lIiA6ICJQYXRpZW50IgogICAgICAgICB9IF0KICAgICAgfSwKICAgICAgInN0YXRlbWVudHMiIDogewogICAgICAgICAiZGVmIiA6IFsgewogICAgICAgICAgICAibG9jYXRvciIgOiAiNzoxLTc6MTUiLAogICAgICAgICAgICAibmFtZSIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJjb250ZXh0IiA6ICJQYXRpZW50IiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAidHlwZSIgOiAiU2luZ2xldG9uRnJvbSIsCiAgICAgICAgICAgICAgICJvcGVyYW5kIiA6IHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjc6MS03OjE1IiwKICAgICAgICAgICAgICAgICAgImRhdGFUeXBlIiA6ICJ7aHR0cDovL2hsNy5vcmcvZmhpcn1QYXRpZW50IiwKICAgICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9QYXRpZW50IiwKICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlJldHJpZXZlIgogICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiOToxLTEwOjEzIiwKICAgICAgICAgICAgIm5hbWUiIDogIkVuY291bnRlcnMiLAogICAgICAgICAgICAiY29udGV4dCIgOiAiUGF0aWVudCIsCiAgICAgICAgICAgICJhY2Nlc3NMZXZlbCIgOiAiUHVibGljIiwKICAgICAgICAgICAgImV4cHJlc3Npb24iIDogewogICAgICAgICAgICAgICAibG9jYXRvciIgOiAiMTA6My0xMDoxMyIsCiAgICAgICAgICAgICAgICJkYXRhVHlwZSIgOiAie2h0dHA6Ly9obDcub3JnL2ZoaXJ9RW5jb3VudGVyIiwKICAgICAgICAgICAgICAgInRlbXBsYXRlSWQiIDogImh0dHA6Ly9obDcub3JnL2ZoaXIvU3RydWN0dXJlRGVmaW5pdGlvbi9FbmNvdW50ZXIiLAogICAgICAgICAgICAgICAidHlwZSIgOiAiUmV0cmlldmUiCiAgICAgICAgICAgIH0KICAgICAgICAgfSwgewogICAgICAgICAgICAibG9jYXRvciIgOiAiMTI6MS0xMzo2OSIsCiAgICAgICAgICAgICJuYW1lIiA6ICJCb25lIE5hcnJvd2luZyBDb25kaXRpb25zIiwKICAgICAgICAgICAgImNvbnRleHQiIDogIlBhdGllbnQiLAogICAgICAgICAgICAiYWNjZXNzTGV2ZWwiIDogIlB1YmxpYyIsCiAgICAgICAgICAgICJleHByZXNzaW9uIiA6IHsKICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjEzOjMtMTM6NjkiLAogICAgICAgICAgICAgICAiZGF0YVR5cGUiIDogIntodHRwOi8vaGw3Lm9yZy9maGlyfUNvbmRpdGlvbiIsCiAgICAgICAgICAgICAgICJ0ZW1wbGF0ZUlkIiA6ICJodHRwOi8vaGw3Lm9yZy9maGlyL1N0cnVjdHVyZURlZmluaXRpb24vQ29uZGl0aW9uIiwKICAgICAgICAgICAgICAgImNvZGVQcm9wZXJ0eSIgOiAiY2F0ZWdvcnkiLAogICAgICAgICAgICAgICAiY29kZUNvbXBhcmF0b3IiIDogImluIiwKICAgICAgICAgICAgICAgInR5cGUiIDogIlJldHJpZXZlIiwKICAgICAgICAgICAgICAgImNvZGVzIiA6IHsKICAgICAgICAgICAgICAgICAgImxvY2F0b3IiIDogIjEzOjI3LTEzOjY4IiwKICAgICAgICAgICAgICAgICAgIm5hbWUiIDogIlByb2JsZW0gTGlzdCBDb25kaXRpb24gQ2F0ZWdvcnkiLAogICAgICAgICAgICAgICAgICAibGlicmFyeU5hbWUiIDogIkNvbW1vbkN4IiwKICAgICAgICAgICAgICAgICAgInR5cGUiIDogIlZhbHVlU2V0UmVmIgogICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgfSBdCiAgICAgIH0KICAgfQp9" + } ] +} \ No newline at end of file diff --git a/tooling/tooling-cli/bundleResourcesResults/Questionnaire-libraryevaluationtest.json b/tooling/tooling-cli/bundleResourcesResults/Questionnaire-libraryevaluationtest.json new file mode 100644 index 000000000..7ce0134b3 --- /dev/null +++ b/tooling/tooling-cli/bundleResourcesResults/Questionnaire-libraryevaluationtest.json @@ -0,0 +1,328 @@ +{ + "resourceType": "Questionnaire", + "id": "libraryevaluationtest", + "meta": { + "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire" ] + }, + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-library", + "valueCanonical": "http://mycontentig.com/fhir/Library/LibraryEvaluationTest" + } ], + "url": "http://mycontentig.com/fhir/Questionnaire/library-evaluation-test", + "identifier": [ { + "use": "official", + "value": "LIBRARYEVALUATIONTEST" + } ], + "version": "1.0.0", + "name": "LibraryEvaluationTestQuestionnaire", + "title": "Library Evaluation Test Questionnaire", + "status": "active", + "experimental": false, + "subjectType": [ "Patient" ], + "description": "A questionnaire for indication of Total Knee Arthroplasty Procedure.", + "item": [ { + "extension": [ { + "url": "http://mycontentig.com/fhir/ValueSet/boolean-calculation-method", + "valueCode": "one-or-more" + } ], + "linkId": "7AEB32D7BD8E52C7", + "prefix": "1", + "text": "Procedure is indicated for 1 or more of the following.", + "type": "question", + "required": false, + "item": [ { + "extension": [ { + "url": "http://mycontentig.com/fhir/ValueSet/boolean-calculation-method", + "valueCode": "all" + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3", + "prefix": "1.1", + "text": "Degenerative joint disease, as indicated by ALL of the following.", + "type": "question", + "required": false, + "item": [ { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Presence of significant radiographic findings, which may include knee joint destruction, angular deformity, or severe narrowing" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-5DB0D0DA537908E5", + "prefix": "1.1.1", + "text": "Presence of significant radiographic findings, which may include knee joint destruction, angular deformity, or severe narrowing.", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Optimal medical management has been tried and failed (eg, weight loss, analgesics, NSAIDs, physical therapy, activity modification)" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-DE330A145C60953E", + "prefix": "1.1.2", + "text": "Optimal medical management has been tried and failed (eg, weight loss, analgesics, NSAIDs, physical therapy, activity modification).", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Patient has failed or is not candidate for more conservative measures (eg, osteotomy)" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-122E41C63B98DF21", + "prefix": "1.1.3", + "text": "Patient has failed or is not candidate for more conservative measures (eg, osteotomy).", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://mycontentig.com/fhir/ValueSet/boolean-calculation-method", + "valueCode": "one-or-more" + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-76ED5EB736DAB43C", + "prefix": "1.1.4", + "text": "Treatment indicated due to 1 or more of the following.", + "type": "question", + "required": false, + "item": [ { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Disabling Pain Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-76ED5EB736DAB43C-85E212BC49A78F11", + "prefix": "1.1.4.1", + "text": "Disabling pain.", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Functional Disability Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F1CFC1157962C1F3-76ED5EB736DAB43C-CBCEFE8095B0DAB9", + "prefix": "1.1.4.2", + "text": "Functional disability.", + "type": "boolean", + "required": false + } ] + } ] + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Failure of Previous Proximal Tibial or Distal Femoral Osteotomy" + } + } ], + "linkId": "7AEB32D7BD8E52C7-D9E9104ABD48B3ED", + "prefix": "1.2", + "text": "Failure of previous proximal tibial or distal femoral osteotomy", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Post Traumatic Knee Joint Destruction" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F8EDD31510D0A2F2", + "prefix": "1.3", + "text": "Posttraumatic knee joint destruction", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Distal Femur Fracture or Tibial Plateau Fracture in Patient with Osteoporosis" + } + } ], + "linkId": "7AEB32D7BD8E52C7-3F639D0886F5C7E6", + "prefix": "1.4", + "text": "Distal femur fracture or tibial plateau fracture in patient with osteoporosis", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Limb Salvage for Malignancy Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-6E5685BB963AE549", + "prefix": "1.5", + "text": "Limb salvage for malignancy", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Congenital Deformity Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-F023C97A3CDB20FB", + "prefix": "1.6", + "text": "Congenital deformity", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Hemophilic Arthropathy Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-8C31772570DFB19F", + "prefix": "1.7", + "text": "Hemophilic arthropathy", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Pigmented Villonodular Synovitis with Joint Destruction" + } + } ], + "linkId": "7AEB32D7BD8E52C7-8678AD85918E8B45", + "prefix": "1.8", + "text": "Pigmented Villonodular Synovitis with Joint Destruction", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://mycontentig.com/fhir/ValueSet/boolean-calculation-method", + "valueCode": "one-or-more" + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7", + "prefix": "1.9", + "text": "Replacement (revision) of previous arthroplasty, as indicated by 1 or more of the following", + "type": "question", + "required": false, + "item": [ { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Disabling Pain Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-85E212BC49A78F11", + "prefix": "1.9.1", + "text": "Disabling pain", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Functional Disability Conditions" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-CBCEFE8095B0DAB9", + "prefix": "1.9.2", + "text": "Functional disability", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Progressive and Substantial Bone Loss (Osteolysis)" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-6E99CA7593E198B8", + "prefix": "1.9.3", + "text": "Progressive and substantial bone loss (osteolysis)", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Dislocation of Patella" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-A52C4ECD0790AC63", + "prefix": "1.9.4", + "text": "Dislocation of patella", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Aseptic Component Instability" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-60C12321081444AC", + "prefix": "1.9.5", + "text": "Aseptic component instability", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Tissue or Systemic Reaction to Metal Implant" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-E9F8A5E6A4CD194A", + "prefix": "1.9.6", + "text": "Tissue or systemic reaction to metal implant", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Infection" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-C6CABDDE21284E33", + "prefix": "1.9.7", + "text": "Infection", + "type": "boolean", + "required": false + }, { + "extension": [ { + "url": "http://hl7.org/fhir/StructureDefinition/cqf-expression", + "valueExpression": { + "language": "text/cql", + "expression": "Has Periprosthetic Fracture" + } + } ], + "linkId": "7AEB32D7BD8E52C7-59F7A040E7CDBFD7-04A08944B99C076A", + "prefix": "1.9.8", + "text": "Periprosthetic fracture", + "type": "boolean", + "required": false + } ] + } ] + } ] +} \ No newline at end of file diff --git a/tooling/tooling-cli/bundleResourcesResults/ValueSet-condition-problem-list-category.json b/tooling/tooling-cli/bundleResourcesResults/ValueSet-condition-problem-list-category.json new file mode 100644 index 000000000..444dc122b --- /dev/null +++ b/tooling/tooling-cli/bundleResourcesResults/ValueSet-condition-problem-list-category.json @@ -0,0 +1,21 @@ +{ + "resourceType": "ValueSet", + "id": "condition-problem-list-category", + "url": "http://mycontentig.com/fhir/ValueSet/condition-problem-list-category", + "version": "1.0.0", + "name": "Valueset_problem_list_condition_category", + "title": "ValueSet - Problem List Condition Category", + "status": "active", + "experimental": false, + "publisher": "Alphora", + "description": "Problem list condition category.", + "expansion": { + "identifier": "condition-problem-list-category-valueset-expansion", + "timestamp": "2021-12-06T13:47:55-07:00", + "contains": [ { + "system": "http://hl7.org/fhir/condition-category", + "code": "problem-list-item", + "display": "Problem List Item" + } ] + } +} \ No newline at end of file diff --git a/tooling/tooling-cli/results/AdverseEvent.html b/tooling/tooling-cli/results/AdverseEvent.html new file mode 100644 index 000000000..2787f8bd1 --- /dev/null +++ b/tooling/tooling-cli/results/AdverseEvent.html @@ -0,0 +1,74 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.1.0 Adverse Event +

+ +

This QDM to QI Core Mapping for the QDM Datatype "Adverse Event" was reviewed by the CQI WG on March 9, 2018 for QDM 5.3 and updated to QDM 5.4 on June 7, 2018. QDM for "Adverse Event" has no changes between versions 5.3 and 5.4.

+ +

QDM defines Adverse Event as any untoward medical occurrence associated with the clinical care delivery, whether or not considered drug related.

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Author dateTime FHIR Provenance.recorded QDM match to QI Core / FHIR +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The AdverseEvent resource does not include a "when.recorded" timing element. Therefore, this mapping uses FHIR provenance.recorded for the QDM attribute for Adverse Event.
Relevant Period AdverseEvent.date QDM references a relevant period: +The Relevant Period references: + +startTime – the time the adverse event began + +stopTime – the time the adverse event completed + +The reason for indicating a period, or interval, is that adverse events may not be instantaneous (e.g., an incorrect intravenous infusion occurs throughout the infusion time). FHIR references a single point in time - not a Period in QI Core. Most likely the event will be recorded with a single point in time. QDM can reference the dateTime as the beginning of the Relevant Period but and endTime will not be available in QI Core or FHIR. + +In general, the AdverseEvent.RelevantPeriod start and stop times in QDM should both map to AdverseEvent.date.
Code AdverseEvent.suspectEntity.Instance The QDM code attribute indicates a single code or a member of the value set used to represent the specific quality data element. The code attribute explicitly specifies the value set (or direct referenced code) filter such that the query will retrieve only values defined by the QDM data element value or value set. Previous versions of QDM datatypes implicitly refer to attributes about a set of items that are included in a value set. A QDM data element binds the Adverse Event datatype to a value set (or to a direct reference code) indicating the suspect entity that might have caused the event. The reason is the measures have mostly been focused on Adverse Event as an exclusion criterion. Thus the "code" attribute is most consistent with AdverseEvent.suspectEntity.instance rather than the type of reaction.
Type AdverseEvent.reaction The QDM "Type" attribute definition addresses a value set (or a direct referenced code) that describes the reaction. Hence the mapping of QDM "type" to AdverseEvent.reaction in QI Core. +
Example - anaphylactic reaction, hives, etc.
+
+Greater clarity in definition in QDM descriptions in the CQL-based HQMF and in the QDM publication should be helpful. In the future, QDM might consider adding the FHIR concept AdverseEvent.type (incident, near-miss, unsafe condition) and AdverseEvent.category (AE - adverse event Vs PAE - Potential Adverse Event)
Severity AdverseEvent.seriousness QDM matched to QI Core / FHIR
FacilityLocations AdverseEvent.location QDM matched to QI Core / FHIR
Recorder AdverseEvent.recorder QDM matched to QI Core / FHIR
id AdvesreEvent.id QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/AllergyIntolerance.html b/tooling/tooling-cli/results/AllergyIntolerance.html new file mode 100644 index 000000000..662b1ebb2 --- /dev/null +++ b/tooling/tooling-cli/results/AllergyIntolerance.html @@ -0,0 +1,72 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.2.0 Allergy Intolerance +

+ +

This QDM to QI Core Mapping for the QDM Datatype "Allergy/Intolerance" was reviewed by the CQI WG on March 9, 2018 for QDM 5.3 and updated to QDM 5.4 on June 7, 2018. QDM for "Allergy Intolerance" has no changes between versions 5.3 and 5.4.

+ +

Allergy is used to address immune-mediated reactions to a substance such as type 1 hypersensitivity reactions, other allergy-like reactions, including pseudo-allergy.

+ +

Intolerance is a record of a clinical assessment of a propensity, or a potential risk to an individual, to have a non-immune mediated adverse reaction on future exposure to the specified substance, or class of substance.

+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Author dateTime AllergyIntolerance.assertedDate Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Prevalence Period AllergyIntolerance.onset[x] + +AllergyIntolerance.lastOccurrence QDM Prevalence Period addresses the onset dateTime and the abatement dateTime for an allergy. The options provided in QI Core / FHIR include + + +
    +
  1. AllergyIntolerance.reaction.onset,
  2. +
  3. AllergyIntolerance.onset(x) (which allows a period) to allow expression of a timeframe during which the allergy began
  4. +
+ +QDM Guidance: The QDM Prevalence Period start should map to AllergyIntolerance.onset[x] and reference the start of the AllergyIntolerance.onset period. the QDM Allergy/Intolerance Prevalence Period end should map to allergyintolerance.lastOccurrence. Different clinical software products address asserted date indicating the time at which the allergy or intolerance is known. That may be the same as the date entered into the software (i.e., assertedDate). However, providers have the option of entering the time the allergy or intolerance was known to occur.
Code AllergyIntolerance.code The AllergyIntolerance.code should reference the class of substance (perhaps using the substance hierarchy of SNOMED). The QDM "Type" attribute definition addresses a value set (or a direct referenced code) that describes the reaction. Hence the mapping of QDM "type" to AllergyIntolerance.reaction.manifestation in QI Core. QI Core and FHIR use "type" to describe the underlying physiologic mechanism for the reaction (i.e., allergy or intolerance). More clarity in the next version of Greater clarity in definition in QDM descriptions in the CQL-based HQMF and in the QDM publication should be helpful.
Type AllergyIntolerance.reaction.manifestation In the future, QDM might consider adding the FHIR concept AllergyIntolerance.type (Allergy, Intolerance) and AllergyIntolerance.category (food, medication, environment, biologic).
Severity Allergyintolerance.criticality QDM severity is more vague than the FHIR concept criticality which is most consistent with the intent of the QDM attribute (i.e., low risk, high risk).
Source AllergyIntolerance.asserter QDM matched to QI Core / FHIR
Recorder AllergyIntolerance.recorder QDM matched to QI Core / FHIR
id Allergyintolerance.id QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Assessment.html b/tooling/tooling-cli/results/Assessment.html new file mode 100644 index 000000000..e5c88297e --- /dev/null +++ b/tooling/tooling-cli/results/Assessment.html @@ -0,0 +1,193 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.3.0 Assessment +

+ +

This QDM to QI Core Mapping for the QDM Category "Assessment" was reviewed by the CQI WG on March 9, 2018 for QDM version 5.3 and updated to QDM 5.4 on June 7, 2018. QDM 5.4 added the QDM datatype "Assessment, Order" that was not present in QDM version 5.3. In QDM 5.4, the "method" attribute has been removed from "Assessment, Order" and "Assessment, Recommended" but is retained for "Assessment, Performed".

+ + +

QDM defines Assessment as a resource used to define specific observations that clinicians use to guide treatment of the patient. An assessment can be a single question, or observable entity with an expected response, an organized collection of questions intended to solicit information from patients, providers or other individuals, or a single observable entity that is part of such a collection of questions. QDM further defines contexts for assessments: Assessment, Performed and Assessment, Recommended. Each context is mapped to QI Core elements in the following tables. +

+ + +

+ 7.3.1 Assessment, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Assessment, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Order is consistent with the ProcedureRequest.intent concept "order".
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core / FHIR + +Note - ProcedureRequest.occurrence(x) defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
Recorder ProcedureRequest.requester.agent QDM matched to QI Core / FHIR
+ + + +

+ 7.3.2 Assessment, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Assessment, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Recommended addresses expectations a provider gives to a patient. Such recommendations are most consistent with the ProcedureRequest.intent value of "plan" (an intension to ensure something occurs without providing an authorization to act).
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core / FHIR + +Note - ProcedureRequest.occurrence(x) defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
Recorder ProcedureRequest.requester.agent QDM matched to QI Core / FHIR
+ + + +

+ 7.3.3 Assessment, Performed +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Assessment, Performed Observation (the .status metadata allows conformance to the specific QDM datatype context) The context "performed" maps closest to the Observation.status final, amended, or corrected.
Method Observaton.method QDM matched to QI Core / FHIR
Negation Rationale Observation.dataAbsentReason QDM negation rationale addresses absence of the information for a known, and approved reason determined by the measure author. Thus eCQMs use a measure developer-specific value set for negation rationale. The value set for Observation.dataAbsentReason is required so QI Core would need to be an extension of the existing information. Note, the referenced value set is extensible.
Reason Observaton.basedOn QDM matched to QI Core / FHIR
Result Observation.value(x) QDM matched to QI Core / FHIR +Note - QI Core also includes Observation.interpretation and Observation.comment - eCQMs have addressed interpretation as an implementation issue. + +Note that the QDM assessment result (or results for components of results can be numerical values, codes or dateTime entries.
Author dateTime Observation.issued Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The W5 report recommends Observation.issued to address the authorDatetime.
Related To Observation.basedOn QDM matched to QI Core / FHIR + +Alternate name: fulfills
Code Observation.code QDM matched to QI Core / FHIR
Components Observation.component QDM uses components to combine related observations to each other, e.g., a set of independent observations performed within an admission assessment for an inpatient admission. Each observation is unique (e.g., questions about tobacco usage, other substance usage, blood pressure, pulse, skin turgor, etc., etc.) but they are captured during the same patient-provider interaction. Such QDM components should map directly to QI Core / FHIR observations that can be linked to other observations. Other QDM component examples include multiple questions that are part of a validated evaluation tool (e.g., a Braden skin assessment scale). This latter example indicates individual questions that are inherently tied to the Braden scale instrument and the questions and answers do not have inherent value without being part of the instrument. QI Core / FHIR considers each such question as an Observation.component. A general rule of thumb is QI Core / FHIR components apply only when the inherent value of the observation is defined by the parent observation. The result is that the content will determine if a QDM component maps to Observation.code or to Observation.component.code.
Component Code Observation.component.code See "Components" - The content will determine if a QDM component maps to Observation.code or to Observation.component.code
Component Result Observation.component.value(x) See "Components" - The content will determine if a QDM component maps to Observation.value(x) or to Observation.component.value(x) + +Note that the QDM assessment result (or results for components of results can be numerical values, codes or dateTime entries.
id Observation.id QDM matched to QI Core / FHIR
Source Observation.performer QDM matched to QI Core / FHIR + +Note - Observation.performer and Observation.device address the QDM concept of Source; use Observation.device if the observer is a device.
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/CareExperience.html b/tooling/tooling-cli/results/CareExperience.html new file mode 100644 index 000000000..0ae297985 --- /dev/null +++ b/tooling/tooling-cli/results/CareExperience.html @@ -0,0 +1,90 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.4.0 Care Experience +

+ +

Reviewed by the CQI Workgroup in April 2018 based on QDM version 5.3. There are no changes to apply for QDM version 5.4.

+ +

QDM defines Care Experience as the experience a patient has when receiving care or a provider has when providing care. The individual Care Experience datatypes should be consulted for further details. QDM represents two kinds of care experience: Patient Care Experience and Provider Care Experience. The main question for the CQI Workgroup is whether Care Experience should be considered an observation, or is there some other concept present in or missing from FHIR.

+ + + + + +

+ 7.4.1 Patient Care Experience +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Patient Care Experience Observation.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Observation.code QDM matched to FHIR / QI Core
Author dateTime observation.issued QDM matched to FHIR / QI Core - Note: QDM only addresses authorTime; + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Observation resource uses .issued for author time. QI Core also allows a Period during which an observation actually occurs (observation.effective[x]). Patient Care Experience generally address specific documentation of observations. However, the experience may be referencing a specific period of time during which the patient was exposed to the healthcare system (e.g., Patient Care Experience during a hospitalization - i.e., with an admission dateTime and discharge dateTime to define the Relevant Period). Such concepts would use observation.effective[x] QDM does not address the time for which the experience is evaluated. Such a concept might be a new consideration for QDM.
id Observation.id QDM matched to FHIR / QI Core
Source Observation.performer QI Core / FHIR does not have a direct concept of "source" but it does list Observation.performer and Observation.device, both of which would address the QDM concept of Source.
+ + + +

+ 7.4.2 Provider Care Experience +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Provider Care Experience Observation.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Observation.code QDM matched to FHIR / QI Core
Author dateTime observation.issued QDM matched to FHIR / QI Core – Note: QDM only addresses authorTime; +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Observation resource uses .issued for author time. QI Core also allows a Period during which an observation actually occurs (observation.effective[x]). Patient Care Experience generally address specific documentation of observations. However, the experience may be referencing a specific period of time during which the patient was exposed to the healthcare system (e.g., Patient Care Experience during a hospitalization - i.e., with an admission dateTime and discharge dateTime to define the Relevant Period). Such concepts would use observation.effective[x]. QDM does not address the time for which the experience is evaluated. Such a concept might be a new consideration for QDM.
id Observation.id QDM matched to FHIR / QI Core
Source Observation.performer QI Core / FHIR does not have a direct concept of "source" but it does list Observation.performer and Observation.device, both of which would address the QDM concept of Source.
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/CareGoal.html b/tooling/tooling-cli/results/CareGoal.html new file mode 100644 index 000000000..9471fcc55 --- /dev/null +++ b/tooling/tooling-cli/results/CareGoal.html @@ -0,0 +1,64 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.5.0 Care Goal +

+ +

Reviewed by the CQI Workgroup in April, 2018 based on QDM version 5.3. There are no changes to apply based on QDM version 5.4.

+ +

QDM defines Care Goal as a defined target or measure to be achieved in the process of patient care, that is, an expected outcome. A typical goal is expressed as a change in status expected at a defined future time. That change can be an observation represented by other QDM categories (diagnostic tests, laboratory tests, symptoms, etc.) scheduled for some time in the future and with a particular value. A goal can be found in the plan of care (care plan), the structure used by all stakeholders, including the patient, to define the management actions for the various conditions, problems, or issues identified for the target of the plan. This structure, through which the goals and care-planning actions and processes can be organized, planned, communicated, and checked for completion, is represented in the QDM categories as a Record Artifact. A time/date stamp is required.

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Care Goal Goal.status QDM matched to FHIR / QI Core
Code Goal.description QDM matched to FHIR / QI Core
Relevant Period Goal.start(x) The QDM Care Goal Relevant Period references the period between: + + +
    +
  • startTime – when the goal is recorded, and therefore should be considered effective,
  • +
  • stopTime – when the target outcome is expected to be met
  • +
  • Relevant Period should be calculated from the Goal.start(x) to the Goal.target.due(x)
  • +
Related to Goal.addresses QDM matched to FHIR / QI Core, Alternate name: fulfills
Target Outcome Goal.target.detail(x) QDM matched to FHIR
id Goal.id QDM matched to FHIR / QI Core
Source Goal.expressedBy The person responsible for setting the goal.
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Communication.html b/tooling/tooling-cli/results/Communication.html new file mode 100644 index 000000000..a010cff8a --- /dev/null +++ b/tooling/tooling-cli/results/Communication.html @@ -0,0 +1,223 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.6.0 Communication +

+ +

The CQI Workgroup reviewed this content in April 2018 based on QDM version 5.3. The content has been updated to reflect a change in QDM version 5.4. QDM version 5.4 removes the previous QDM datatypes "Communication, Patient to Provider", "Communication, Provider to Patient", and "Communication, Provider to Provider". QDM version 5.4 creates a new QDM datatype, "Communication, Performed". The previous QDM datatype mapping are retained for historical review.

+ +

QDM defines Communication as the transmission, receipt, or acknowledgement of information sent from a source to a recipient, such as from one clinician to another regarding findings, assessments, plans of care, consultative advice, instructions, educational resources, etc. It also may include the receipt of response from a patient with respect to any aspect of the care provided. Furthermore, it may include the conveying of information from provider to patient (e.g., results, findings, plans for care, medical advice, instructions, educational resources, appointments). A time and date stamp is required. QDM defines on QDM datatype, Communication, Performed.

+ +

FHIR Communication Resource

+

This resource is a record of a communication. A communication is a conveyance of information from one entity, a sender, to another entity, a receiver. The sender and receivers may be patients, practitioners, related persons, organizations, or devices. Communication use cases include: +

+
    +
  • A reminder or alert delivered to a responsible provider
  • +
  • A recorded notification from the nurse that a patient's temperature exceeds a value
  • +
  • A notification to a public health agency of a patient presenting with a communicable disease reportable to the public health agency
  • +
  • Patient educational material sent by a provider to a patient
  • +
  • Non-patient specific communication use cases may include:
  • +
  • A nurse call from a hall bathroom
  • +
  • Advisory for battery service from a pump
  • +
+ + +

HL7 FHIR Procedure Resource

+

The boundary between determining whether an action is a Procedure (training or counseling) as opposed to a Communication is based on whether there's a specific intent to change the mind-set of the patient. Mere disclosure of information would be considered a Communication. A process that involves verification of the patient's comprehension or to change the patient's mental state would be a Procedure.

+ + + + +

+ 7.6.1 QDM 5.4 ONLY Communication, Performed +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Communication, Performed Communication.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Communication.reasonCode Note - CommunicationRequest in QI Core could be considered but QDM does not differentiate a Communication recommended or ordered, only communication so the mapping assumes the communication has occurred, with or without a response.
Category Communication.category QDM matched to FHIR / QI Core
Sender Communication.sender QDM matched to FHIR / QI Core
Recipient Communication.recipient QDM matched to FHIR / QI Core
Negation Rationale Communication.notDoneReason QDM matched to FHIR / QI Core
Medium Communication.medium QDM matched to FHIR / QI Core
Relevant Period Communication.sent +
Communication.received
+
QDM matched to FHIR / QI Core; Note - if interoperability is not present, the sending system may not have acknowledgement that the communication was received. In such situation, the QDM relevant period will have no end dateTime.
Author dateTime Communication.sent QDM addresses authorDatetime as the time the communication is sent.
Related to Communication.basedOn An order, proposal or plan fulfilled in whole or in part by this Communication.
id Communication.id QDM matched to FHIR / QI Core
Source Communication.sender The entity (e.g. person, organization, clinical information system, or device) which was the source of the communication.
+ + + +

+ 7.6.2 QDM 5.3 ONLY Communication, Patient to Provider +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Communication, Patient to Provider Communication.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Communication.reasonCode Note - CommunicationRequest in QI Core could be considered but QDM does not differentiate a Communication recommended or ordered, only communication so the mapping assumes the communication has occurred, with or without a response.
Negation Rationale Communication.notDoneReason QDM matched to FHIR / QI Core
Author dateTime Communication.sent QDM addresses authorDatetime as the time the communication is sent.
Related to Communication.basedOn An order, proposal or plan fulfilled in whole or in part by this Communication.
id Communication.id QDM matched to FHIR / QI Core
Source Communication.sender The entity (e.g. person, organization, clinical information system, or device) which was the source of the communication.
+ + +

+ 7.6.3 QDM 5.3 ONLY Communication, Provider to Patient +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Communication, Provider to Patient Communication.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Communication.reasonCode Note - CommunicationRequest in QI Core could be considered but QDM does not differentiate a Communication recommended or ordered, only communication so the mapping assumes the communication has occurred, with or without a response.
Negation Rationale Communication.notDoneReason QDM matched to FHIR / QI Core
Author dateTime Communication.sent QDM addresses authorDatetime as the time the communication is sent.
Related to Communication.basedOn An order, proposal or plan fulfilled in whole or in part by this Communication.
id Communication.id QDM matched to FHIR / QI Core
Source Communication.sender The entity (e.g. person, organization, clinical information system, or device) which was the source of the communication.
+ + +

+ 7.6.4 QDM 5.3 ONLY Communication, Provider to Provider +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Communication, Provider to Provider Communication.status QDM matched to FHIR / QI Core (the .status metadata allows conformance to the specific QDM datatype context)
Code Communication.reasonCode Note - CommunicationRequest in QI Core could be considered but QDM does not differentiate a Communication recommended or ordered, only communication so the mapping assumes the communication has occurred, with or without a response.
Negation Rationale Communication.notDoneReason QDM matched to FHIR / QI Core
Author dateTime Communication.sent QDM addresses authorDatetime as the time the communication is sent.
Related to Communication.basedOn An order, proposal or plan fulfilled in whole or in part by this Communication.
id Communication.id QDM matched to FHIR / QI Core
Source Communication.sender The entity (e.g. person, organization, clinical information system, or device) which was the source of the communication.
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/ConditionDiagnosisProblem.html b/tooling/tooling-cli/results/ConditionDiagnosisProblem.html new file mode 100644 index 000000000..7fc71519a --- /dev/null +++ b/tooling/tooling-cli/results/ConditionDiagnosisProblem.html @@ -0,0 +1,64 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.7.0 Condition Diagnosis Problem +

+ +

This QDM to QI Core Mapping for the QDM Datatype "Diagnosis" was reviewed by the CQI WG on March 30, 2018 for QDM version 5.3. There are no changes in "Condition/Diagnosis/Problem" in QDM version 5.4.

+ +

QDM defines Condition/Diagnosis/Problem as a practitioner’s identification of a patient’s disease, illness, injury, or condition. This category contains a single datatype to represent all of these concepts: Diagnosis. A practitioner determines the diagnosis by means of examination, diagnostic test results, patient history, and/or family history. Diagnoses are usually considered unfavorable, but may also represent neutral or favorable conditions that affect a patient’s plan of care (e.g., pregnancy).

+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Diagnosis Condition (the Condition.clinicalStatus metadata allows conformance to the specific QDM datatype context) QDM defaults the status to active and prevalence period provides the evidence of activity. Note: The FHIR Condition resource does not specify a status for Condition.clinicalStatus; however, the QDM datatype Diagnosis default state is "active"; hence, the Condition.clinicalStatus should be constrained to "active". Also, the Condition.verificationStatus should be constrained to "confirmed".
Prevalence Period Condition.onset[x], OR Condition.asserted Use Condition.onset[x], if present, use Condition.asserted as a fallback. Note that it may not be appropriate to map Condition.asserted to Diagnosis.prevalencePeriod.start when Condition.status is inactive. QDM matched to QI Core / FHIR for start of Prevalence Period
+Condition.abatement[x] QDM matched to QI Core / FHIR for end of Prevalence Period
Anatomical Location Site Condition.bodySite QDM matched to QI Core / FHIR
Severity Condition.severity QDM matched to QI Core / FHIR
Code Condition.code QDM matched to QI Core / FHIR
Author dateTime Condition.assertedDate QDM matched to QI Core / FHIR
id Condition.id QDM matched to QI Core / FHIR
Source Condition.asserter QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Device.html b/tooling/tooling-cli/results/Device.html new file mode 100644 index 000000000..7e56bf031 --- /dev/null +++ b/tooling/tooling-cli/results/Device.html @@ -0,0 +1,164 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.8.0 Device +

+ + + +

This QDM to QI Core Mapping for the QDM Datatype "Device" was reviewed by the CQI WG on April 6, 2018 for QDM version 5.3. The content presented here is based on QDM version 5.4 and removes the QDM 5.3 attribute "anatomical approach site" as there was no existing use or clear use case to retain it.

+

QDM defines Device as an instrument, apparatus, implement, machine, contrivance, implant, in-vitro reagent, or other similar or related article, including a component part or accessory, intended for use in the diagnosis, cure, mitigation, treatment, or prevention of disease and not dependent on being metabolized to achieve any of its primary intended purposes. QDM provides three contexts for expressing devices: Device, Applied; Device, Order; and Device, Recommended. +

+
    +
  1. Note that the current use of the QDM datatype “Device, Applied� usually references the procedure to “apply� the device (i.e., to use for the patient, to use on the patient’s body, or to implant in the patient’s body). Each of these current uses should address the concept using the QDM datatypes, Intervention, Performed, or Procedure, Performed.
  2. +
  3. The original intent of the QDM datatype “Device, Applied� was to reference the specific device or type of device of concern to the measure. And that reference might be as detailed a providing a device class as referenced in a Unique Device Identifier (UDI). The FHIR Device resource (http://hl7.org/fhir/device.html) does have a metadata element for UDI (http://hl7.org/fhir/device-definitions.html#Device.udi). All device metadata reference inherent qualities of the device. And timing addresses Device.manufactureDate and Device.expirationDate. Therefore, a direct map to the Device resource may not be sufficient or appropriate for the QDM Device, Applied datatype.
  4. +
  5. The FHIR Resource DeviceUseStatement (http://hl7.org/fhir/deviceusestatement.html) is a more appropriate resource to map QDM Device, Applied. The DeviceUseStatement has a maturity level of 0, so the mapping will need to be updated as the resource is updated. Note: this mapping will need to be reviewed with the QI Core DeviceUseStatement to assure alignment since the current QI Core content does not seem to address the most recent version of DeviceUseStatement.
  6. +
+ + + + +

+ 7.8.1 Device, Applied +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Device Applied DeviceUseStatement A record of a device being used by a patient where the record is the result of a report from the patient or another clinician.
Anatomical Location Site DeviceUseStatement.bodySite Indicates the site on the subject's body where the device was used ( i.e. the target site).
Negation Rationale None A DeviceUseStatement does not address negation directly since there would be no statement if the device were absent. QDM usage is to indicate devices that have not been applied should address negation rationale for the order or the recommendation (i.e., ProcedureRequest.extension (reasonRefused)). One could address negation rationale by indicating DeviceUseStatement.status constrained to values stopped or on-hold and reference DeviceUseStatement.reason to indicate the reason (i.e., reason for not using the device).
Reason DeviceUseStatement.indication Reason or justification for use of the device.
Relevant Period DeviceUseStatement.whenUsed The time period over which the device was used. +Note – the DeviceUseStatement resource also has a metadata element: DeviceUseStatement.timing(x) indicating how often the device was used (may be a period or dateTime).
Author dateTime DeviceUseStatement.recordedOn The time at which the DeviceUseStatement was made/recorded.
Code DeviceUseStatement.device + +Device.udi.deviceIdentifier DeviceUseStatemtn.device references Device for details of the device used. Device.udi.deviceIdentifier (DI) is a mandatory, fixed portion of a Unique Device Identified (UDI) that identifies the labeler and the specific version or model of a device.
id DeviceUseStatement.id Logical id of this artifact
Source DeviceUseStatement.source Who reported this device was being used by the patient.
+ + + +

+ 7.8.2 Device, Order +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Device Order DeviceRequest.intent DeviceRequest.intent uses the concepts draft, active, on-hold, revoked, completed, entered-in-error, unkown. "Draft" is most consistent with the DeviceRequest when applied to clinical decision support (CDS) or expectations a provider gives to a patient. "active" or "completed" is consistent with the intent of the QDM Device, Order datatype.
Negation Rationale None DeviceRequest resource does not include a reason the device request did not occur. QI Core does have a ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused). One could address negation rationale by indicating DeviceRequest.status constrained to values suspended or cancelled and reference DeviceRequest.reasonCode to indicate the reason (i.e., reason for not using the device).
Reason DeviceRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime DeviceRequest.AuthoredOn Note - DeviceRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code DeviceRequest.code Device requested
id DeviceRequest.id QDM matched to QI Core / FHIR
Source DeviceRequest.requester Who, what is requesting the diagnositics - note also, DeviceRequest.performer to indicate the requested filler of the request.
+ + + +

+ 7.8.3 Device, Recommended +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Device Recommended DeviceRequest.intent DeviceRequest.intent uses the concepts draft, active, on-hold, revoked, copmleted, enetered-in-error, unknown. "draft" is most consistent with the DeviceRequest when applied to clinical decision support (CDS) or expectations a provider gives to a patient. "active" or "completed" is consistent with the intent of the QDM Device, Order datatype.
Negation Rationale None DeviceRequest resource does not include a reason the device request did not occur. QI Core does have a ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused). See FHIR Tracker Item 15939.
Reason DeviceRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime DeviceRequest.AuthoredOn Note - DeviceRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code DeviceRequest.code Device requested
id DeviceRequest.id QDM matched to QI Core / FHIR
Source DeviceRequest.requester Who, what is requesting the diagnositics - note also, DeviceRequest.performer to indicate the requested filler of the request.
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/DiagnosticStudy.html b/tooling/tooling-cli/results/DiagnosticStudy.html new file mode 100644 index 000000000..dc1915e77 --- /dev/null +++ b/tooling/tooling-cli/results/DiagnosticStudy.html @@ -0,0 +1,266 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.9.0 Diagnostic Study +

+ + +

The content on this page was updated based on review by the CQI Workgroup Conference call on April 6, 2018 with reference to QDM 5.3. The content presented here is updated to QDM version 5.4. QDM version 5.4 removed the "method" attribute from "Diagnostic Study, Order" and "Diagnostic Study, Recommended" but retained the "method" attribute for "Diagnostic Study, Performed". There are no other changes in QDM version 5.4 as compared to QDM version 5.3.

+ +

QDM defines Diagnostic Study as any kind of medical test performed as a specific test or series of steps to aid in diagnosing or detecting disease (e.g., to establish a diagnosis, measure the progress or recovery from disease, confirm that a person is free from disease). The QDM defines diagnostic studies as those that are not performed in organizations that perform testing on samples of human blood, tissue, or other substance from the body. Diagnostic studies may make use of digital images and textual reports. Such studies include but are not limited to imaging studies, cardiology studies (electrocardiogram, treadmill stress testing), pulmonary-function testing, vascular laboratory testing, and others. QDM defines three contexts for Diagnostic Study: Diagnostic Study, Order; Diagnostic Study, Performed; and Diagnostic Study, Recommended. Note that QI Core maps for imaging type diagnostic studies are mapped to DiagnosticReport; QI Core maps for non-imaging type diagnostic studies are mapped to Observation. +

+ + +

+ 7.9.1 Diagnostic Study, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Diagnostic Study, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode, Use http://hl7.org/fhir/us/qicore/qicore-procedurerequest-appropriatenessScore.html to address RAND criteria for appropriate imaging usage criteria. QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core/FHIR. Note - ProcedureRequest.occurrence(x) defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code See also, Observation.category
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.9.2 Diagnostic Study, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Diagnostic Study, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Recommended addresses expectations a provider gives to a patient. Such recommendations are most consistent with the ProcedureRequest.intent value of "plan" (an intension to ensure something occurs without providing an authorization to act).
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode, Use http://hl7.org/fhir/us/qicore/qicore-procedurerequest-appropriatenessScore.html to address RAND criteria for appropriate imaging usage criteria. QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core/FHIR. Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code See also, Observation.category
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.9.3 Diagnostic Study, Performed (Imaging Studies) +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Diagnostic Study, Performed (Specific to Imaging Studies) DiagnosticReport (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR. Quality measures and CDS artifacts may need to address which status is desired: Partial, preliminary, final, amended, corrected, appended. Most quality measures will address final, amended, corrected, appended.
FacilityLocation DiagnosticReport.extension (diagnosticReport-locationPerformed) QDM matched to QI Core / FHIR
Method ImagingStudy.modalityList.code QDM matched to QI Core/FHIR
Negation Rationale None FHIR DiagnosticReport does not address negation since there would be no report. QDM usage to indicate studies that have not been done should address negation rationale for the order or the recommendation (i.e., ProcedureRequest.extension (reasonRefused).
Reason None FHIR DiagnosticReport does not address reason since reason is a function of the order or recommendation. QDM reference to indicate the reason for imaging studies should reference qicore-procedurerequest-appropriatenessScore.
Result DiagnosticReport.result Note - QI Core also includes Observation.interpretation and Observation.comment - eCQMs have addressed interpretation as an implementation issue. Perhaps that approach should be reconsidered.
Result dateTime DiagnosticReport.issued QDM matched to QI Core / FHIR
Relevant Period DiagnosticReport.effective(x) QDM addresses a Period; QI Core allows an effective time. Consider addressing the difference.
Status DiagnosticReport.status QDM matched to QI Core / FHIR
Code DiagnosticReport.code Note - QI Core includes DiagnosticReport.specimen - eCQMs use the LOINC value set to determine the specimen source
Author dateTime DiagnosticReport.issued QDM matched to FHIR / QI Core – Note: QDM only addresses authorTime; +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Observation resource uses .issued for author time. QI Core also allows a Period during which an observation actually occurs (observation.effective[x]). Patient Care Experience generally address specific documentation of observations. However, the experience may be referencing a specific period of time during which the patient was exposed to the healthcare system (e.g., Patient Care Experience during a hospitalization - i.e., with an admission dateTime and discharge dateTime to define the Relevant Period). Such concepts would use observation.effective[x]. QDM does not address the time for which the experience is evaluated. Such a concept might be a new consideration for QDM
Components Observation.component QDM uses components to combine related observations to each other, e.g., a set of independent observations performed within an admission assessment for an inpatient admission. Each observation is unique (e.g., questions about tobacco usage, other substance usage, blood pressure, pulse, skin turgor, etc., etc.) but they are captured during the same patient-provider interaction. Such QDM components should map directly to QI Core / FHIR observations that can be linked to other observations. Other QDM component examples include multiple questions that are part of a validated evaluation tool (e.g., a Braden skin assessment scale). This latter example indicates individual questions that are inherently tied to the Braden scale instrument and the questions and answers do not have inherent value without being part of the instrument. QI Core / FHIR considers each such question as an Observation.component. A general rule of thumb is QI Core / FHIR components apply only when the inherent value of the observation is defined by the parent observation. The result is that the content will determine if a QDM component maps to Observation.code or to Observation.component.code.
Component Code Observation.component.code See "component" - content-specific
Component Result Observation.component.value(x) See "component" - content-specific +
id DiagnosticReport.id QDM matched to QI Core / FHIR
Source DiagnosticReport.performer Source is addressed by either DiagnosticReport.performer or Observation.device
+ + + +

+ 7.9.4 Diagnostic Study, Performed (Non-Imaging Studies) +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Diagnostic Study, Performed (Specific to Non-Imaging Studies) Observation (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR. Quality measures and CDS artifacts may need to address which status is desired: Partial, preliminary, final, amended, corrected, appended. Most quality measures will address final, amended, corrected, appended.
FacilityLocation DiagnosticReport.extension (diagnosticReport-locationPerformed) QI Core observation does not specify a facility location. FHIR provenance may provider guidance.
Method Observaton.method QDM matched to QI Core / FHIR
Negation Rationale Observation.dataAbsentReason The value set is required. Many eCQMs use a measure developer-specific value set. Note, component also has a Observation.component.dataAbsentReason in QI Core.
Reason Observaton.basedOn This is the best fit in QI Core
Result Observation.value(x) Note - QI Core also includes Observation.interpretation and Observation.comment - eCQMs have addressed interpretation as an implementation issue. Perhaps that approach should be reconsidered.
Result dateTime Observation.issued QDM matched to QI Core / FHIR
Relevant Period Observation.effective[x] QDM addresses a Period; QI Core allows an effective time. Consider addressing the difference.
Status Observation.status QDM matched to QI Core / FHIR
Code Observation.code Note - QI Core includes Observation.specimen - eCQMs use the LOINC value set to determine the specimen source
Author dateTime Observation.issued QDM matched to FHIR / QI Core – Note: QDM only addresses authorTime; +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Observation resource uses .issued for author time. QI Core also allows a Period during which an observation actually occurs (observation.effective[x]). Patient Care Experience generally address specific documentation of observations. However, the experience may be referencing a specific period of time during which the patient was exposed to the healthcare system (e.g., Patient Care Experience during a hospitalization - i.e., with an admission dateTime and discharge dateTime to define the Relevant Period). Such concepts would use observation.effective[x]. QDM does not address the time for which the experience is evaluated. Such a concept might be a new consideration for QDM
Components Observation.component QDM uses components to combine related observations to each other, e.g., a set of independent observations performed within an admission assessment for an inpatient admission. Each observation is unique (e.g., questions about tobacco usage, other substance usage, blood pressure, pulse, skin turgor, etc., etc.) but they are captured during the same patient-provider interaction. Such QDM components should map directly to QI Core / FHIR observations that can be linked to other observations. Other QDM component examples include multiple questions that are part of a validated evaluation tool (e.g., a Braden skin assessment scale). This latter example indicates individual questions that are inherently tied to the Braden scale instrument and the questions and answers do not have inherent value without being part of the instrument. QI Core / FHIR considers each such question as an Observation.component. A general rule of thumb is QI Core / FHIR components apply only when the inherent value of the observation is defined by the parent observation. The result is that the content will determine if a QDM component maps to Observation.code or to Observation.component.code.
Component Code Observation.component.code See "component" - content-specific
Component Result Observation.component.value(x) See "component" - content-specific +
id Observation.id QDM matched to QI Core / FHIR
Source Observation.performer Source is addressed by either Observation.performer or Observation.device
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Encounter.html b/tooling/tooling-cli/results/Encounter.html new file mode 100644 index 000000000..7abcdba2e --- /dev/null +++ b/tooling/tooling-cli/results/Encounter.html @@ -0,0 +1,187 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.10.0 Encounter +

+ +

The HL7 CQI Workgroup reviewed the content on this page in April 2018 referencing QDM version 5.3. There are no changes to the QDM Encounter category in QDM 5.4.

+ + +

QDM defines Encounter as an identifiable grouping of healthcare-related activities characterized by the entity relationship between the subject of care and a healthcare provider; such a grouping is determined by the healthcare provider. A patient encounter represents interaction between a healthcare provider and a patient with a face-to-face patient visit to a clinician’s office, or any electronically remote interaction with a clinician for any form of diagnostic treatment or therapeutic event. Encounters can be billable events but are not limited to billable interactions. Each encounter has an associated location or modality within which it occurred (such as an office, home, electronic methods, phone encounter, or telemedicine methods). The encounter location is the patient’s location at the time of measurement. Different levels of interaction can be specified in the value associated with the element while modes of interaction (e.g., telephone) may be modeled using the data flow attribute. QDM defines three contexts for Encounter: Encounter, Order; Encounter, Performed; Encounter, Recommended. Note: Encounter, Performed; Encounter, Order; and Encounter, Recommended do not include a clear mechanism to address urgent, emergent or routine (scheduled) encounters. QDM is evaluating the possibility of adding Encounter,priority to address these issues. See the QDM Known Issues site to review the request and to link to the ONC Jira QDM Issues page to join in the discussion in consideration of the next version of QDM (5.5) [1].

+ + + + +

+ 7.10.1 Encounter, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Encounter, Order ReferralRequest.intent - FHIR STU 4.0 updating to ServiceRequest.intent Referral Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. For QDM datatypes with the context "order" the ReferralRequest.intent value shall be constrained to "order"
FacilityLocation ReferralRequest.context QDM matched to QI Core / FHIR
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) Modeled as a procedure request - there is no Encounter request in QI Core. Note FHIR STU 4.0 is updating ReferralRequest to Service.Request. See FHIR Tracker Item 15940 for negation rationale for ServiceRequest.
Reason ReferralRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ReferralRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM
Code ReferralRequest.type See also, Observation.category
id ReferralRequest.id QDM matched to QI Core / FHIR
Source ReferralRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.10.2 Encounter, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Encounter, Recommended ReferralRequest.intent Referral Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. For QDM datatypes with the context "recommended" the ReferralRequest.intent value shall be constrained to "order"
FacilityLocation ReferralRequest.context QDM matched to QI Core / FHIR
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) Modeled as a procedure request - there is no Encounter request in QI Core. Note FHIR STU 4.0 is updating ReferralRequest to Service.Request. See FHIR Tracker Item 15940 for negation rationale for ServiceRequest.
Reason ReferralRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ReferralRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM
Code ReferralRequest.type See also, Observation.category
id ReferralRequest.id QDM matched to QI Core / FHIR
Source ReferralRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.10.3 Encounter, Performed +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Encounter, Performed Encounter (the .status metadata allows conformance to the specific QDM datatype context) Constrained to status consistent with in-progress or finished.
Relevant Period Encounter.period QDM matched to QI Core / FHIR
Admission Source Encounter.hospitalization.admitSource QDM matched to QI Core / FHIR
Diagnosis Encounter.diagnosis QDM matched to QI Core / FHIR
Discharge Disposition Encounter.hospitalization.dischargeDisposition QDM matched to QI Core / FHIR
Length of Stay Encounter.length QDM matched to QI Core / FHIR
Negation Rationale Encounter.extension (http://hl7.org/fhir/StructureDefinition/encounter-reasonCancelled) Only applies to an encounter that was cancelled, not an encounter that was never planned for a specific reason.
Principal Diagnosis Encounter.diagnosis.role + +Encounter.diagnosis.rank Constrain Encounter.diagnosis.role to "Billing Diagnosis" and require Encounter.diagnosis.rank =1. See FHIR Tracker Request 15944 requesting addition of Principal Diagnosis reference in US Core that is consistent with this QDM to QI Core mapping
Author dateTime FHIR.provenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources. Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Encounter resource does not reference an author dateTime; hence the QDM mapping references FHIR.provenance.
Code Encounter.type Encounter.type fits best with the type of encounter. The Encounter.type value set includes codes from SNOMED CT decending from 308335008 patient encounter procedure (procedure) and codes from the Current Procedure and Terminology designated for Evaluation and Management (99200 – 99607) (subscription to AMA Required). Code systems included: +http://snomed.info/sct +http://www.ama-assn.org/go/cpt + +Encounter.type indicates: The codes SHALL be taken from US Core Encounter Type; other codes may be used where these codes are not suitable
FacilityLocations Encounter.location QDM matched to QI Core / FHIR
FacilityLocations code Location.type QDM matched to QI Core / FHIR
FacilityLocations location period Location.period QDM matched to QI Core / FHIR
id Encounter.id QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/FamilyHistory.html b/tooling/tooling-cli/results/FamilyHistory.html new file mode 100644 index 000000000..a927517b1 --- /dev/null +++ b/tooling/tooling-cli/results/FamilyHistory.html @@ -0,0 +1,50 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.11.0 Family History +

+ +

The HL7 CQI Workgroup reviewed the content on this page in April 2018 with respect to QDM version 5.3. There are no changes to the QDM category "Family History" in QDM version 5.4.

+

QDM defines Family History as a diagnosis or problem experienced by a family member of the patient. Typically, a family history will not contain very much detail, but the simple identification of a diagnosis or problem in the patient’s family history may be relevant to the care of the patient.

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Family History FamilyMemberHistory.status QDM matched to QI Core / FHIR
Author dateTime FamilyMemberHistory.date QDM matched to QI Core / FHIR
Relationships FamilyMemberHistory.relationship QDM matched to QI Core / FHIR
Code FamilyMemberHistory.condition.code QDM matched to QI Core / FHIR
id FamilyMemberHistory.id QDM matched to QI Core / FHIR
Source FamilyMemberHistory.patient QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Immunization.html b/tooling/tooling-cli/results/Immunization.html new file mode 100644 index 000000000..e23b3e8bf --- /dev/null +++ b/tooling/tooling-cli/results/Immunization.html @@ -0,0 +1,131 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.12.0 Immunization +

+ +

QDM defines Immunization as vaccines administered to patients in healthcare settings but does not include non-vaccine agents. QDM defines two contexts for immunization: Immunization, Administered; Immunization, Order.

+ +

The HL7 CQI Workgroup reviewed and updated this content on April 24, 2018 based on QDM version 5.3. The content was updated on June 7, 2018 consistent with QDM version 5.4. The only change between QDM versions 5.3 and 5.4 is that version 5.4 removes the "supply" attribute from "Immunization, Administered" and retains the "supply" attribute for "Immunization, Order"

+ + +

FHIR Immunization Recommendation resource is specifically designed to provide an immunization forecast from a forecasting engine to a provider, basically to carry clinical decision support recommendations specific to immunizations and, therefore, is not consistent with the intent of the QDM datatype "Immunization, Order". The most closely associated resource is MedicationRequest. There is no comparable FHIR resource to specify ImmunizationRequest. + +

+ + +

+ 7.12.1 Immunization, Administered +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Immunization, Administered Immunization (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Dosage Immunization.doseQuantity QDM matched to QI Core / FHIR
Negation Rationale Immunization.explanation.reasonNotGiven QDM matched to QI Core / FHIR
Reason Immunization.explanation.reason QDM matched to QI Core / FHIR
Route Immunization.route QDM matched to QI Core / FHIR
Author dateTime Immunization.date QDM matched to QI Core / FHIR + +NOTE: See QDM Known Issues for update – Immunization, Administered should include only the dateTime of the immunization administration. Until QDM 5.5 version, implementers should map Immunization, Administered author dateTime to administrationTime. [1]
Code Immunization.vaccineCode QDM matched to QI Core / FHIR
id Immunization.id QDM matched to QI Core / FHIR
Source Immunization.primarySource Source addressed by primarySource or reportOrigin
+ + + +

+ 7.12.2 Immunization, Order +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Immunization, Order MedicationRequest.intent Medication Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Active dateTime MedicationRequest.dispenseRequest.validityPeriod This indicates the validity period of a prescription (stale dating the Prescription).
Dosage MedicationRequest.dosageInstruction Indicates how the medication is to be used by the patient.
Negation Rationale MedicationStatement.reasonNotTaken Medication Request in QI Core does not reference negation rationale. See FHIR Tracker Item 15941 addressing notDoneReason requirement.
Reason MedicationRequest.reasonCode QDM matched to QI Core / FHIR
Route MedicationRequest.dosageInstruction.route QDM matched to QI Core / FHIR.
Author dateTime MedicationRequest.authoredOn QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Supply MedicationRequest.dispenseRequest.quantity QDM matched to QI Core / FHIR.
Code MedicationRequest.medication QDM matched to QI Core / FHIR
id MedicationRequest.id QDM matched to QI Core / FHIR
Source MedicationRequest.requester QDM matched to FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/IndividualCharacteristic.html b/tooling/tooling-cli/results/IndividualCharacteristic.html new file mode 100644 index 000000000..fb9271d02 --- /dev/null +++ b/tooling/tooling-cli/results/IndividualCharacteristic.html @@ -0,0 +1,242 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.13.0 Individual Characteristic +

+ +

The CQI Workgroup reviewed this content based on QDM version 5.3 in April 2018. There are no changes to the content for QDM version 5.4.

+ +

QDM defines Individual Characteristic as specific factors about a patient, clinician, provider, or facility. Included are demographics, behavioral factors, social or cultural factors, available resources, and preferences. Behaviors reference responses or actions that affect (either positively or negatively) health or healthcare. Included in this category are mental health issues, adherence issues unrelated to other factors or resources, coping ability, grief issues, and substance use/abuse. Social/cultural factors are characteristics of an individual related to family/caregiver support, education, and literacy (including health literacy), primary language, cultural beliefs (including health beliefs), persistent life stressors, spiritual and religious beliefs, immigration status, and history of abuse or neglect. Resources are means available to a patient to meet health and healthcare needs, which might include caregiver support, insurance coverage, financial resources, and community resources to which the patient is already connected and from which the patient is receiving benefit. Preferences are choices made by patients and their caregivers relative to options for care or treatment (including scheduling, care experience, and meeting of personal health goals) and the sharing and disclosure of their health information. QDM defines nine individual characteristics: Patient Characteristic; Patient Characteristic, Birthdate; Patient Characteristic, Clinical Trial Participant; Patient Characteristic, Ethnicity; Patient Characteristic, Expired; Patient Characteristic, Payer; Patient Characteristic, Race; Patient Characteristic, Sex; Provider Characteristic.

+ + + +

+ 7.13.1 Patient Characteristic (Generic) +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Author dateTime FHIR Provenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources.
Code +Not sufficiently specific to address with FHIR
id patient.id QDM matched to FHIR / QI Core
Source +Not sufficiently specific to address with FHIR
Recorder FHIR Provenance.recorder QDM matched to FHIR / QI Core
+ + +

+ 7.13.2 Patient Characteristic: Birthdate +

+ + +
+ + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Birth dateTime Patient.birthDate + +Patient.extension.birthTime Patient.extension.birthTime used for birthTime for newborns.
Code +Not addressed in QI Core / FHIR
id patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.3 Patient Characteristic: Clinical Trial Participant +

+ +
+ + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Reason Patient.extension.clinicalTrial.reason QDM matched to QI Core / FHIR
Relevant Period Patient.extension.clinicalTrial.period QDM matched to QI Core / FHIR
Code Patient.extension.clinicalTrial.NCT QDM matched to QI Core / FHIR
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.4 Patient Characteristic: Ethnicity +

+ +
+ + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Code Patient.extension (http://hl7.org/fhir/us/core/StructureDefinition-us-core-ethnicity) .ombCategory QDM matched to QI Core / FHIR
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.5 Patient Characteristic: Expired +

+ +
+ + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Cause +Not addressed in QI Core / FHIR
Expiration dateTime Patient.deceasedDateTime QDM matched to QI Core / FHIR
Code deceasedBoolean If the code exisits in QDM the deceasedBoolean is true. Uses a direct referenced code (SNOMED CT 419099009 2016-03).
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.6 Patient Characteristic: Payer +

+ +
+ + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Relevant Period +Not addressed in QI Core / FHIR
Code +Not addressed in QI Core / FHIR
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.7 Patient Characteristic: Race +

+ +
+ + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Code Patient.extension (http://hl7.org/fhir/us/core/StructureDefinition-us-core-race.html) .ombCategory QDM matched to QI Core / FHIR
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.8 Patient Characteristic: Sex +

+ +
+ + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Code Patient.gender QDM matched to QI Core / FHIR
id Patient.id QDM matched to QI Core / FHIR
+ + +

+ 7.13.9 Provider Characteristic +

+ +
+ + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Author dateTime FHIR Provenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources. Note - FHIR includes a concept Practitioner.qualification.period (period during which the qualification is valid). The qualification period may be useful in defining CDS or eCQMs.
Code Practitioner.qualification.code QDM matched to QI Core / FHIR
id Practitioner.identifier.id QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Intervention.html b/tooling/tooling-cli/results/Intervention.html new file mode 100644 index 000000000..0bbe1e8a8 --- /dev/null +++ b/tooling/tooling-cli/results/Intervention.html @@ -0,0 +1,315 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.14.0 Intervention +

+ + +

The CQI Workgroup reviewed this page in April 2018 consistent with QDM version 5.3. There are no updates to the content required based on QDM version 5.4 (reviewed June 7, 2018); however, on June 15, 2018, the CQI Workgroup reviewed the QDM FHIR mappings for Intervention and determined there is some ambiguity requiring implementer comment. +

+
    +
  • Note that while the QDM categories Intervention and Procedure have been modeled identically in previous versions of QI Core, the FHIR Task Resource provides a potentially more effective way to differentiate Interventions from Procedures. Both are retained in the QDM as they are more clinically expressive for clinicians to understand the measure intent.
  • +
  • The QDM to FHIR mapping provided here show mapping to both Procedure/ProcedureRequest and to Task.
  • +
  • The FHIR resource definitions for Procedure and Task seem to have significant overlap - see FHIR Tracker item 17359
  • +
  • Both mappings (i.e., Intervention mapped to Procedure/ProcedureRequest and to Task) are shown here. Comments are welcome regarding the most appropriate use.
  • +
+ + +

QDM defines Intervention as a course of action intended to achieve a result in the care of persons with health problems that does not involve direct physical contact with a patient. Examples include patient education and therapeutic communication. QDM defines three contexts for intervention: Intervention, Order; Intervention, Performed; Intervention, Recommended.

+ + + + +

+ 7.14.1 Intervention, Order [Using ProcedureRequest] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.14.2 Intervention, Recommended [Using ProcedureRequest] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Recommended addresses expectations a provider gives to a patient. Such recommendations are most consistent with the ProcedureRequest.intent value of "plan" (an intension to ensure something occurs without providing an authorization to act).
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.14.3 Intervention, Performed [Using Procedure] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Performed Procedure (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Relevant Period Procedure.performed[x] QDM matched to QI Core / FHIR
Negation Rationale Procedure.notDoneReason QDM matched to QI Core / FHIR
Reason Procedure.reasonCode QDM matched to QI Core / FHIR
Result Procedure.outcome QDM matched to QI Core / FHIR
Author dateTime FHIR.provenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources. Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). Hence, the QDM Procedure author dateTime references FHIR provenance.
Status Procedure.status QDM matched to QI Core / FHIR + +Note: Procedure.status allows specification of completed but it does not assure the procedure was completed successfully. The FHIR resource Proecedure.outcome does also specification of successful completion, but that element may be ambiguous. The Patient Care Workgroup is evaluating options to more clearly represent a procedure that successfully achieved its objective (objective is not currently available as a metadata element). Moreover, clinical care capture of success with respect to procedure completion is highly variable. See the QDM Known Issues for a description of the issue and a link to the ONC Jira QDM Issues page to join the discussion for QDM [1].
Code Procedure.code QDM matched to QI Core / FHIR
id Procedure.id QDM matched to QI Core / FHIR
Source Procedure.performer QDM matched to QI Core / FHIR
+ + + +

+ 7.14.4 Intervention, Order [Using Task] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Order Task.intent Task intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Negation Rationale Task.statusReason QDM matched to QI Core/FHIR - Constrained to instances in which Task.status = rejected, cancelled, or failed Task status value set (required)
Reason Task.reason QDM matched to QI Core / FHIR
Author dateTime Task.authoredOn QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code Task.code QDM matched to QI Core / FHIR
id Task.id QDM matched to QI Core / FHIR
Source Task.requester Task also provides the opportunity to request Task.owner (the person responsible for the task) and task.performerType
+ + + +

+ 7.14.5 Intervention, Recommended [Using Task] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Order Task.intent Task intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "plan" from the intent value set for QDM datatypes with the recommended context.
Negation Rationale | row2cell2 | Task.statusReason QDM matched to QI Core/FHIR - Constrained to instances in which Task.status = rejected, cancelled, or failed Task status value set (required)
Reason Task.reason QDM matched to QI Core / FHIR
Author dateTime Task.authoredOn QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code Task.code QDM matched to QI Core / FHIR
id Task.id QDM matched to QI Core / FHIR
Source Task.requester Task also provides the opportunity to request Task.owner (the person responsible for the task) and task.performerType
+ + + +

+ 7.14.6 Intervention, Performed [Using Task] +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Intervention, Performed Task (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Relevant Period Task.executionPeriod QDM matched to QI Core / FHIR
Negation Rationale Task.statusReason QDM matched to QI Core/FHIR - Constrained to instances in which Task.status = rejected, cancelled, or failed Task status value set (required)
Reason Task.reason QDM matched to QI Core / FHIR
Result Task.output.value[x] QDM matched to QI Core / FHIR
Author dateTime Task.authoredOn QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Status Task.status QDM matched to QI Core / FHIR
Code Task.code QDM matched to QI Core / FHIR
id Task.id QDM matched to QI Core / FHIR
Source Task.requester Task also provides the opportunity to request Task.owner (the person responsible for the task) and task.performerType
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/LaboratoryTest.html b/tooling/tooling-cli/results/LaboratoryTest.html new file mode 100644 index 000000000..eddae6d1e --- /dev/null +++ b/tooling/tooling-cli/results/LaboratoryTest.html @@ -0,0 +1,199 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.15.0 Laboratory Test +

+ + +

The content on this page was reviewed by the CQI Workgroup Conference call on April 6, 2018 for QDM version 5.3. The page is updated to reference QDM version 5.4. The only changes present in QDM version 5.4 is removal of the "method" attribute for "Laboratory Test, Order" and "Laboratory Test, Recommended". The QDM datatype "Laboratory Test, Performed" retains the "method" attribute in QDM version 5.4.

+ +

QDM defines Laboratory Test as a medical procedure that involves testing a sample of blood, urine, or other substance from the body. Tests can help determine a diagnosis, plan treatment, check to see if treatment is working, or monitor the disease over time. This QDM data category for Laboratory Test is only used for information about the subject of record. QDM defines three contexts for Laboratory Test: Laboratory Test, Order; Laboratory Test, Performed; Laboratory Test, Recommended.

+ + + + +

+ 7.15.1 Laboratory Test, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Laboratory Test, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode, Use http://hl7.org/fhir/us/qicore/qicore-procedurerequest-appropriatenessScore.html to address RAND criteria for appropriate imaging usage criteria. QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core/FHIR. Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code See also, Observation.category
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.15.2 Laboratory Test, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Laboratory Test, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Recommended addresses expectations a provider gives to a patient. Such recommendations are most consistent with the ProcedureRequest.intent value of "plan" (an intension to ensure something occurs without providing an authorization to act).
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode, Use http://hl7.org/fhir/us/qicore/qicore-procedurerequest-appropriatenessScore.html to address RAND criteria for appropriate imaging usage criteria. QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn QDM matched to QI Core/FHIR. Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code See also, Observation.category
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization on whose behalf the device or practitioner was acting
+ + + +

+ 7.15.3 Laboratory Test, Performed +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Laboratory Test, Performed Observation (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Method Observaton.method QDM matched to QI Core / FHIR
Negation Rationale Observation.dataAbsentReason The value set is required. Many eCQMs use a measure developer-specific value set. Note, component also has a Observation.component.dataAbsentReason in QI Core.
Reason Observation.basedOn This is the best fit in QI Core
Reference Range High Observation.referenceRange.high QI Core also references Observation.referenceRange.type - i.e. what part of the referenced target population it applies to (e.g., normal or therapeutic range); and Observation.referenceRange.appliesTo - i.e., the target population (normal population, or particular sex or race), and Observation.referenceRange.age. QI Core also addresses Observation.component.referenceRange. Should these options be considered for QDM?
Reference Range Low Observation.referenceRange.low QI Core also references Observation.referenceRange.type - i.e. what part of the referenced target population it applies to (e.g., normal or therapeutic range); and Observation.referenceRange.appliesTo - i.e., the target population (normal population, or particular sex or race), and Observation.referenceRange.age. QI Core also addresses Observation.component.referenceRange. Should these options be considered for QDM?
Result Observation.value(x) Observation.value(x) refers to QI Core Observation which is noted here. Note - QI Core also includes Observation.interpretation and Observation.comment - eCQMs have addressed interpretation as an implementation issue. Perhaps that approach should be reconsidered.
Result dateTime Observation.issued QDM matched to QI Core / FHIR
Relevant Period Observaton.effective[x] QDM matched to QI Core / FHIR
Status Observation.status QDM matched to QI Core / FHIR
Code Observation.code Note - QI Core includes Observation.specimen - eCQMs use the LOINC value set to determine the specimen source
Author dateTime Observation.issued QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Components Observation.component QDM uses components to combine related observations to each other, e.g., a set of independent observations performed within an admission assessment for an inpatient admission. Each observation is unique (e.g., questions about tobacco usage, other substance usage, blood pressure, pulse, skin turgor, etc., etc.) but they are captured during the same patient-provider interaction. Such QDM components should map directly to QI Core / FHIR observations that can be linked to other observations. Other QDM component examples include multiple questions that are part of a validated evaluation tool (e.g., a Braden skin assessment scale). This latter example indicates individual questions that are inherently tied to the Braden scale instrument and the questions and answers do not have inherent value without being part of the instrument. QI Core / FHIR considers each such question as an Observation.component. A general rule of thumb is QI Core / FHIR components apply only when the inherent value of the observation is defined by the parent observation. The result is that the content will determine if a QDM component maps to Observation.code or to Observation.component.code.
Component Code Observation.component.code See "Component" - Content-specific
Component Result Observation.component.value(x) See "Component" - Content-specific
Component Reference Range High Observation.component.referenceRange Refers back to Component.referenceRange
Component Reference Range Low Observation.component.referenceRange Refers back to Component.referenceRange
id Observation.id QDM matched to QI Core / FHIR
Source Observation.performer Note - QI Core includes Observation.subject - QDM should consider adding subject instead of health record field
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Medication.html b/tooling/tooling-cli/results/Medication.html new file mode 100644 index 000000000..3ddfb27b0 --- /dev/null +++ b/tooling/tooling-cli/results/Medication.html @@ -0,0 +1,316 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.16.0 Medication +

+ +

The CQI Workgroup reviewed this QDM category in April 2018 consistent with QDM version 5.3. The content is updated on June 7, 2018 consistent with QDM version 5.4. The only differences between the two QDM versions are: +

+
    +
  • Removal of the "supply" attribute from "Medication, Active", and "Medication, Administered". ["supply" is retained for "Medication, Order", "Medication, Dispensed", and "Medication, Discharge".
  • +
  • Addition of a "setting" attribute to "Medication, Order" to allow differentiation of the setting in which the patient is to use the medication (consistent with the MedicationRequest.category FHIR metadata element).
  • +
+ + +

QDM defines Medication as clinical drugs or chemical substances intended for use in the medical diagnosis, cure, treatment, or prevention of disease. Medications are defined as direct referenced values or value sets containing values derived from code systems such as RxNorm. QDM defines five contexts for Medication: Medication, Active; Medication, Administered; Medication, Discharge; Medication, Dispensed; Medication, Order. + +

+ + +

+ 7.16.1 Medication, Active +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Medication, Active MedicationStatement (the .status metadata allows conformance to the specific QDM datatype context) The Medication, Active datatype requires use of the "active" status code from the FHIR value set
Dosage MedicationStatement.dosage.dose QDM matched to QI Core / FHIR
Frequency MedicationStatement.dosage.timing QDM matched to QI Core / FHIR
Route MedicationStatement.dosage.route QDM matched to QI Core / FHIR
Relevant Period MedicationStatement.effective QDM matched to QI Core / FHIR
Code MedicationStatement.medication[x] QDM matched to QI Core / FHIR
id IMedicationStatement.id QDM matched to QI Core / FHIR
Source MedicationStatement.informationSource Source addressed by primarySource or reportOrigin
+ + + +

+ 7.16.2 Medication, Administered +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Medication, Administered MedicationAdministration (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Dosage MedicationAdministration.dosage.dose QDM matched to QI Core / FHIR
Frequency MedicationStatement.dosage.timing Medication Administration documents a single administration, thus frequency is not a concept in the Medication Administered resource.
Negation Rationale MedicationAdministration.reasonNotGiven QDM matched to QI Core / FHIR
Reason MedicationAdministration.reasonCode QDM matched to QI Core / FHIR
Route MedicationAdministration.dosage.route QDM matched to QI Core / FHIR
Relevant Period MedicationAdministration.effective[x] QDM matched to QI Core / FHIR
Author dateTime FHIRprovenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources. Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR MedicationAdministration resource does not reference an author dateTime. Hence, the QDM Medication, Administered datatype author dateTime references FHIR provenance.
Code MedicationAdministration.medication[x] QDM matched to QI Core / FHIR
id MedicationAdministration.id QDM matched to QI Core / FHIR
+ + + +

+ 7.16.3 Medication, Dispensed +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Medication, Dispensed MedicationDispense (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Dosage MedicationDispense.quantity QDM matched to QI Core / FHIR
Supply MedicationDispense.daysSupply QDM matched to QI Core / FHIR
Frequency MedicationDispense.dosageInstruction.timing QDM matched to QI Core / FHIR
Negation Rationale MedicationDispense.notDoneReason[x] QDM matched to QI Core / FHIR
Refills MedicationDispense.extension (http://hl7.org/fhir/StructureDefinition/pharmacy-core-refillsRemaining) QDM matched to QI Core / FHIR
Route MedicationDispense.dosageInstruction.route QDM matched to QI Core / FHIR
Relevant Period MedicationDispense.extension (http://hl7.org/fhir/StructureDefinition/medicationdispense-validityPeriod) QDM matched to QI Core / FHIR
Author dateTime FHIR.provenance.dateTime FHIR references authorDatetime as part of the provenance resource for all resources. + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). FHIR MedicationDispense does not reference an author time. Hence, the QDM Medication, Dispensed datatype references FHIR provenance.
Code MedicationDispense.medication[x] QDM matched to QI Core / FHIR
id MedicationDispense.id QDM matched to QI Core / FHIR
Source MedicationDispense.performer QDM matched to QI Core / FHIR
+ + + +

+ 7.16.4 Medication, Discharge +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Medication, Discharge MedicationRequest.intent New profile created to address Medication-request-category = dischargeMedication (LOINC code 8654-6 Hospital Discharge medications) bound to RxNorm. The MedicationRequest.Intent is constrained to "plan" to address recommendations to the patient. The MedicationRequest.category content does not yet include discharge medications.
Dosage MedicationRequest.dosageInstruction.dose[x] Note - modeled using MedicationRequest - could also be modeled as MedicationStatement with a status of "intended". May be modeled better as a component of a discharge instruction set but I can't find that in QI Core or FHIR
Supply +Supply shouldn't be required for Medication, Discharge since it is a set of medication instructions for a patient after departing from the hospital. However, in some cases, EHRs incorporate data from medication orders (prescriptions) written for post-discharge use into the discharge medication list provided to patients. In such cases, the supply may be present for newly prescribed medications. Such information is not covered in MedicationStatement in QI Core.
Frequency MedicationRequest.dosageInstruction.timing Note - MedicationRequest.dosageInstruction.timing includes both the frequency instruction (e.g., every 8 hours) and the start and stop dates for the amount included.
Negation Rationale MedicationStatement.reasonNotTaken Medication Request in QI Core does not reference negation rationale
Refills MedicationRequest.dispenseRequest.numberOfRepeatsAllowed The mapping is somewhat problematic. The Medication, Discharge maps to MedicationRequest using the .intent metadata element constrained the "plan" - i.e., recommendation to the patient about which medications to take after discharge. While MedicationRequest has a .dispenseRequest.numberOfRepeatsAllowed, that metadata element is relevant for a prescription and not a medication list provided as a patient recommendation. Consider retiring this "refills" attribute for Medication, Discharge in QDM
Route MedicationRequest.dosageInstruction.route QDM matched to QI Core / FHIR
Author dateTime MedicationRequest.authoredOn Start of relevant period - no specific author time in QI Core. In the case of Medication, Discharge, the "request" is either to the patient (for medications present in the home or those obtained over-the-counter) or a reference to the prescription sent to the pharmacy that is included in the discharge medication instructions. +
Note MedicationRequest.dispenseRequest.validityPeriod (Consider prescription validity as a QDM attribute)
+MedicationRequest.dosageInstruction.timing (Note - MedicationRequest.dosageInstruction.timing includes both the frequency instruction (e.g., every 8 hours) and the start and stop dates for the amount included.)
+
Code MedicationRequest.reasonCode QDM matched to QI Core / FHIR
id MedicationRequest.id QDM matched to QI Core / FHIR
+ + + +

+ 7.16.5 Medication, Order +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Medication, Order MedicationRequest.intent Medication Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Dosage MedicationRequest.dosageInstruction.dose[x] QDM matched to QI Core / FHIR
Supply MedicationRequest.dispenseRequest.quantity QDM matched to QI Core / FHIR
Frequency MedicationRequest.dosageInstruction.timing QDM matched to QI Core / FHIR
Setting MedicationRequest.category QDM matched to QI Core / FHIR
Code MedicationRequest.medication QDM matched to QI Core / FHIR
Method MedicationRequest.dosageInstruction.method QDM matched to QI Core / FHIR
Negation Rationale MedicationStatement.reasonNotTaken Medication Request in QI Core does not reference negation rationale. See FHIR Tracker Item 15941 addressing notDoneReason requirement.
Reason MedicationRequest.reasonCode QDM matched to QI Core / FHIR
Refills MedicationRequest.dispenseRequest.numberOfRepeatsAllowed QDM matched to QI Core / FHIR
Relevant Period MedicationRequest.dispenseRequest.expectedSupplyDuration QDM matched to QI Core / FHIR, except that expectedSupplyDuration is an interval, to map to relevantPeriod, it needs to be anchored to a starting date. Use start of dispenseRequest.validityPeriod as the starting date for the expectedSupplyDuration.
Route MedicationRequest.dosageInstruction.route QDM matched to QI Core / FHIR
Author dateTime MedicationRequest.authoredOn QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
id MedicationRequest.id QDM matched to QI Core / FHIR
Source MedicationRequest.requester QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Participation.html b/tooling/tooling-cli/results/Participation.html new file mode 100644 index 000000000..7699db1d8 --- /dev/null +++ b/tooling/tooling-cli/results/Participation.html @@ -0,0 +1,43 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.17.0 Participation +

+ +

The CQI Workgroup reviewed this content in April 2018 based on QDM version 5.3. There are no changes required based on QDM version 5.4.

+ +

QDM defines Participation as a patient’s coverage by a program such as an insurance or medical plan or a payment agreement. Such programs can include patient-centered medical home, disease-specific programs, etc. Definitions modeled similar to HL7 FHIR STU 3.0 - https://www.hl7.org/fhir/coverage.html. +

+ + + +
+ + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Participation Coverage (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to FHIR / QI Core
Code Coverage.type Modeled with FHIR Resource Coverage - may not apply to non-financial program participation but there is no other clear option identified. Not in QI Core
Participation Period Coverage.period QDM matched to FHIR / QI Core
id Coverage.id QDM matched to FHIR / QI Core
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/PhysicalExam.html b/tooling/tooling-cli/results/PhysicalExam.html new file mode 100644 index 000000000..b9d532348 --- /dev/null +++ b/tooling/tooling-cli/results/PhysicalExam.html @@ -0,0 +1,199 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.18.0 Physical Exam +

+ + +

The CQI Workgroup reviewed the mapping to QDM version 5.3 in April 2018. The content is updated on June 7, 2018 based on QDM version 5.4. The changes based on QDM 5.4 include: +

+
    +
  • Removal of the "method" attribute from "Physical Exam, Order" and "Physical Exam, Recommended"
  • +
+ + +

QDM defines Physical Exam as the evaluation of the patient's body and/or mental status exam to determine its state of health. The techniques of examination can include palpation (feeling with the hands or fingers), percussion (tapping with the fingers), auscultation (listening), visual inspection or observation, inquisition and smell. Measurements may include vital signs (blood pressure, pulse, respiration) as well as other clinical measures (such as expiratory flow rate and size of lesion). Physical exam includes psychiatric examinations. QDM defines three contexts for Physical Exam: Physical Exam, Order; Physical Exam, Performed; Physical Exam, Recommended.

+ + + + +

+ 7.18.1 Physical Exam, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Physical Exam, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Anatomical Location Site ProcedureRequest.extension (http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedurerequest-approachBodySite) Note QI Core ProcedureRequest.extension (http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedurerequest-appropriatenessScore) - not included in QDM but may potentially apply to Appropriate Use Criteria
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.18.2 Physical Exam, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Physical Exam, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Anatomical Location Site ProcedureRequest.extension (http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedurerequest-approachBodySite) Note QI Core ProcedureRequest.extension (http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-procedurerequest-appropriatenessScore) - not included in QDM but may potentially apply to Appropriate Use Criteria
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.18.3 Physical Exam, Performed +

+ +
    +
  • Note - Physical Exam, Performed uses the US FHIR Core Observation Profile referenced by QI Core. Specifically for Vital Signs, QI Core addresses the US FHIR Core Vital Signs Profile which specifies LOINC Codes and definitions for each references vital sign and Observation.component.code for each of diastolic blood pressure and systolic blood pressure.
  • +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Physical Exam, Performed Observation (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Anatomical Location Site Observation.bodySite QDM matched to QI Core / FHIR
Method Observaton.method QDM matched to QI Core / FHIR
Relevant Period observation.effective.x QDM matched to QI Core / FHIR
Negation Rationale Observation.dataAbsentReason The value set is required. Many eCQMs use a measure developer-specific value set. Note, component also has a Observation.component.dataAbsentReason in QI Core.
Reason Observaton.basedOn QDM matched to QI Core / FHIR
Result Observation.value(x) Note - QI Core also includes Observation.interpretation and Observation.comment - eCQMs have addressed interpretation as an implementation issue. Perhaps that approach should be reconsidered.
Author dateTime Observation.issued QDM matched to QI Core / FHIR + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Status Procedure.status QDM matched to QI Core / FHIR
Components Observation.component QDM uses components to combine related observations to each other, e.g., a set of independent observations performed within an admission assessment for an inpatient admission. Each observation is unique (e.g., questions about tobacco usage, other substance usage, blood pressure, pulse, skin turgor, etc., etc.) but they are captured during the same patient-provider interaction. Such QDM components should map directly to QI Core / FHIR observations that can be linked to other observations. Other QDM component examples include multiple questions that are part of a validated evaluation tool (e.g., a Braden skin assessment scale). This latter example indicates individual questions that are inherently tied to the Braden scale instrument and the questions and answers do not have inherent value without being part of the instrument. QI Core / FHIR considers each such question as an Observation.component. A general rule of thumb is QI Core / FHIR components apply only when the inherent value of the observation is defined by the parent observation. The result is that the content will determine if a QDM component maps to Observation.code or to Observation.component.code.
Component Code Observation.component.code See "Component" - Content-specific
Component Result Observation.component.value(x) See "Component" - Content-specific
Code Observation.code QDM matched to QI Core / FHIR
id Observation.id QDM matched to QI Core / FHIR
Source Observation.performer QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file diff --git a/tooling/tooling-cli/results/Procedure.html b/tooling/tooling-cli/results/Procedure.html new file mode 100644 index 000000000..22c634838 --- /dev/null +++ b/tooling/tooling-cli/results/Procedure.html @@ -0,0 +1,210 @@ +--- +# jekyll header +--- +{% include header.html %} +{% include container-start.html %} + + +

+ 7.19.0 Procedure +

+ + +

The CQI Workgroup reviewed this page in April 2018 consistent with QDM version 5.3. QDM version 5.4 updates were applied on June 7, 2018: +

+
    +
  • Removal of "anatomical approach site" from "Procedure, Performed", "Procedure, Order", and "Procedure, Recommended"
  • +
  • Removal of "method" from "Procedure, Order" and "Procedure, Recommended"
  • +
+ + +
Note that while the QDM categories Intervention and Procedure are modeled identically and interoperability standards do not differentiate between them, both are retained in the QDM as they are more clinically expressive for clinicians to understand the measure intent.
+
+ +

QDM defines Procedure as “An Act whose immediate and primary outcome (post-condition) is the alteration of the physical condition of the subject. … Procedure is but one among several types of clinical activities such as observation, substance-administrations, and communicative interactions … Procedure does not comprise all acts of [sic] whose intent is intervention or treatment.� A procedure may be a surgery or other type of physical manipulation of a person’s body in whole or in part for purposes of making observations and diagnoses or providing treatment. QDM defines three contexts for Procedure: Procedure, Order; Procedure, Performed; Procedure, Recommended.

+ + + + +

+ 7.19.1 Procedure, Order +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Procedure, Order ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. Constrain to "order" from the intent value set for QDM datatypes with the order context.
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
AnatomicalLocation ProcedureRequest.bodySite QDM matched to QI Core / FHIR
Ordinality . Not addressed in FHIR ProcedureRequest
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.19.2 Procedure, Recommended +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Procedure, Recommended ProcedureRequest.intent Procedure Request intent uses the concepts proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option. "Proposal" is most consistent with the ProcedureRequest when applied to clinical decision support (CDS) in which the CDS proposes an action to a provider or to a patient. The QDM concept Recommended addresses expectations a provider gives to a patient. Such recommendations are most consistent with the ProcedureRequest.intent value of "plan" (an intension to ensure something occurs without providing an authorization to act).
Negation Rationale ProcedureRequest.extension (http://hl7.org/fhir/StructureDefinition/procedurerequest-reasonRefused) QDM matched to QI Core / FHIR
Reason ProcedureRequest.reasonCode QDM matched to QI Core / FHIR
Author dateTime ProcedureRequest.authoredOn Note - ProcedureRequest.occurrence[x] defines a dateTime when the event should occur - not addressed in QDM + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html).
Code ProcedureRequest.code QDM matched to QI Core / FHIR
AnatomicalLocation ProcedureRequest.bodySite QDM matched to QI Core / FHIR
Ordinality . Not addressed in FHIR ProcedureRequest
id ProcedureRequest.id QDM matched to QI Core / FHIR
Source ProcedureRequest.requester Author, orderer - note also, ProcedureRequest.requester.agent for device, practitioner or organization who initiated the request; or ProcedureRequest.requester.onBehalfOf - the organization the device or practitioner was acting on behalf of
+ + + +

+ 7.19.3 Procedure, Performed +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QDM Attribute QI Core Metadata Element Comment
Procedure, Performed Procedure (the .status metadata allows conformance to the specific QDM datatype context) QDM matched to QI Core / FHIR
Relevant Period Procedure.performed[x] QDM matched to QI Core / FHIR
Negation Rationale Procedure.notDoneReason QDM matched to QI Core / FHIR
Reason Procedure.reasonCode QDM matched to QI Core / FHIR
Result Procedure.outcome QDM matched to QI Core / FHIR
AnatomicalLocation Procedure.bodySite QDM matched to QI Core / FHIR
IncisionDateTime Procedure.extension ([CanonicalType[1]] QDM matched to QI Core / FHIR
Method . Not addressed in FHIR
Ordinality . Not addressed in FHIR
Components . Not addressed in FHIR
Author dateTime FHIR.provenance.recorded FHIR references authorDatetime as part of the provenance resource for all resources. + +Note: FHIR Provenance generally addresses the author of the message; the identifier/source of the original resource element is defined by the resource. Individual resource element provenance is summarized in the FHIR W5 Report (http://hl7.org/fhir/w5.html). The FHIR Procedure resource does not address an author time. Hence, the QDM Procedure, Performed datatype references FHIR provenance.
Status Procedure.status QDM matched to QI Core / FHIR + +Note: Procedure.status allows specification of completed but it does not assure the procedure was completed successfully. The FHIR resource Proecedure.outcome does also specification of successful completion but that element may be ambiguous. The Patient Care Workgroup is evaluating options to more clearly represent a procedure that successfully achieved its objective (objective is not currently available as a metadata element). Moreover, clinical care capture of success with respect to procedure completion is highly variable. See the QDM Known Issues for a description of the issue and a link to the ONC Jira QDM Issues page to join the discussion for QDM [2].
Code Procedure.code QDM matched to QI Core / FHIR
id Procedure.id QDM matched to QI Core / FHIR
Source Procedure.performer QDM matched to QI Core / FHIR
+ +{% include container-end.html %} +{% include footer.html %} \ No newline at end of file