![JAR search and dependency download from the Maven repository](/logo.png)
org.opencds.cqf.cql.execution.CqlEngine Maven / Gradle / Ivy
package org.opencds.cqf.cql.execution;
import org.cqframework.cql.elm.execution.ExpressionDef;
import org.cqframework.cql.elm.execution.FunctionDef;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.UsingDef;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.opencds.cqf.cql.data.DataProvider;
import org.opencds.cqf.cql.terminology.TerminologyProvider;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* NOTE: Several possible approaches to traversing the ELM tree for execution:
*
* 1. "Executable" Node Hierarchy: Create nodes for each ELM type and deserialize into these nodes
* This option works well, but is problematic for maintenance because Java doesn't have partial classes.
* There also doesn't seem to be a way to tell JAXB which hierarchy to use if you have two different hierarchies
* for the same schema (Trackable-based ELM used by the CQL-to-ELM translator, and Executable-based ELM used by the engine).
* This could potentially be a bonus though, as it forces the engine to not take a dependency on the translator, forcing
* a clean separation between the translator and the engine.
*
* 2. Visitor Pattern: This option is potentially simpler to implement, however:
* a. The visitor pattern doesn't lend itself well to aggregation of results, which is the real work of each node anyway
* b. Extensibility is compromised, difficult to introduce new nodes (unlikely to be a practical issue though)
* c. Without lambdas, the cost of traversal is quite high due to the expensive if-then-else chains in the visitor nodes
*
* So, opting for the Executable Node Hierarchy for now, knowing that it creates a potential maintenance issue, but
* this is acceptable because the ELM Hierarchy is settling down, and so long as all the non-generated code is at the
* end, this should be easy to maintain. In addition, it will be much more performant, and lend itself much better to
* the aggregation of values from child nodes.
*/
public class CqlEngine {
public static enum Options {
EnableExpressionCaching
}
private LibraryLoader libraryLoader;
private Map dataProviders;
private TerminologyProvider terminologyProvider;
private EnumSet options;
public CqlEngine(LibraryLoader libraryLoader) {
this(libraryLoader, null, null);
}
public CqlEngine(LibraryLoader libraryLoader, Map dataProviders, TerminologyProvider terminologyProvider) {
this(libraryLoader, dataProviders, terminologyProvider, EnumSet.of(Options.EnableExpressionCaching));
}
// TODO: External function provider
public CqlEngine(LibraryLoader libraryLoader, Map dataProviders, TerminologyProvider terminologyProvider, EnumSet options) {
if (libraryLoader == null) {
throw new IllegalArgumentException("libraryLoader can not be null.");
}
this.libraryLoader = libraryLoader;
this.dataProviders = dataProviders;
this.terminologyProvider = terminologyProvider;
this.options = options;
}
public EvaluationResult evaluate(Map> expressions)
{
return this.evaluate(null, null, expressions);
}
public EvaluationResult evaluate(VersionedIdentifier libraryIdentifier)
{
return this.evaluate(null, null, libraryIdentifier);
}
public EvaluationResult evaluate(Map contextParameters, Map> parameters, VersionedIdentifier libraryIdentifier)
{
Library library = this.loadLibrary(libraryIdentifier);
Map> expressions = this.getExpressionMap(library);
return this.evaluate(contextParameters, parameters, expressions);
}
public EvaluationResult evaluate(Map contextParameters, Map> parameters, Map> expressions)
{
Map libraries = this.loadLibraries(expressions.keySet());
return this.evaluate(contextParameters, parameters, expressions, libraries);
}
private EvaluationResult evaluate(Map contextParameters, Map> parameters, Map> expressions, Map libraries) {
EvaluationResult evaluationResult = new EvaluationResult();
for (Map.Entry> entry : expressions.entrySet()) {
if (!libraries.containsKey(entry.getKey())) {
throw new IllegalArgumentException(String.format("Library %s required to evaluate expressions and was not found.",
this.getLibraryDescription(entry.getKey())));
}
Library library = libraries.get(entry.getKey());
Context context = this.setupContext(contextParameters, parameters, library);
LibraryResult result = this.evaluateLibrary(context, library, entry.getValue());
evaluationResult.libraryResults.put(entry.getKey(), result);
}
return evaluationResult;
}
private LibraryResult evaluateLibrary(Context context, Library library, Set expressions) {
LibraryResult result = new LibraryResult();
for (String expression : expressions) {
ExpressionDef def = context.resolveExpressionRef(expression);
// TODO: We should probably move this validation further up the chain.
// For example, we should tell the user that they've tried to evaluate a function def through incorrect
// CQL or input parameters. And the code that gather the list of expressions to evaluate together should
// not include function refs.
if (def instanceof FunctionDef) {
continue;
}
context.enterContext(def.getContext());
Object object = def.evaluate(context);
result.expressionResults.put(expression, object);
}
return result;
}
// TODO: Handle global parameters?
private Context setupContext(Map contextParameters, Map> parameters, Library library) {
// Context requires an initial library to init properly.
// TODO: Allow context to be initialized with multiple libraries
Context context = new Context(library);
// TODO: Does the context actually need a library loaded if all the libraries are prefetched?
// We'd have to make sure we include the dependencies too.
context.registerLibraryLoader(this.libraryLoader);
if (this.options.contains(Options.EnableExpressionCaching)) {
context.setExpressionCaching(true);
}
if (this.terminologyProvider != null) {
context.registerTerminologyProvider(this.terminologyProvider);
}
if (this.dataProviders != null) {
for (Map.Entry pair : this.dataProviders.entrySet()) {
context.registerDataProvider(pair.getKey(), pair.getValue());
}
}
if (contextParameters != null) {
for (Map.Entry pair : contextParameters.entrySet()) {
context.setContextValue(pair.getKey(), pair.getValue());
}
}
if (parameters != null) {
for (Map.Entry> libraryParameters : parameters.entrySet()) {
for (Map.Entry parameterValue : libraryParameters.getValue().entrySet()) {
context.setParameter(libraryParameters.getKey().getId(), parameterValue.getKey(), parameterValue.getValue());
}
}
}
return context;
}
private Map loadLibraries(Set libraryIdentifiers) {
Map libraries = new HashMap<>();
for (VersionedIdentifier libraryIdentifier : libraryIdentifiers) {
Library library = this.loadLibrary(libraryIdentifier);
libraries.put(libraryIdentifier, library);
}
return libraries;
}
private Library loadLibrary(VersionedIdentifier libraryIdentifier) {
Library library = this.libraryLoader.load(libraryIdentifier);
if (library == null) {
throw new IllegalArgumentException(String.format("Unable to load library %s",
libraryIdentifier.getId() + libraryIdentifier.getVersion() != null ? "-" + libraryIdentifier.getVersion() : ""));
}
// TODO: Removed this validation pending more intelligent handling at the service layer
// For example, providing a mock or dummy data provider in the event there's no data store
//this.validateDataRequirements(library);
this.validateTerminologyRequirements(library);
// TODO: Optimization ?
// TODO: Validate Expressions as well?
return library;
}
private void validateDataRequirements(Library library) {
if (library.getUsings() != null && library.getUsings().getDef() != null && !library.getUsings().getDef().isEmpty())
{
for (UsingDef using : library.getUsings().getDef()) {
// Skip system using since the context automatically registers that.
if (using.getUri().equals("urn:hl7-org:elm-types:r1"))
{
continue;
}
if (this.dataProviders == null || !this.dataProviders.containsKey(using.getUri())) {
throw new IllegalArgumentException(String.format("Library %1$s is using %2$s and no data provider is registered for uri %2$s.",
this.getLibraryDescription(library.getIdentifier()),
using.getUri()));
}
}
}
}
private void validateTerminologyRequirements(Library library) {
if ((library.getCodeSystems() != null && library.getCodeSystems().getDef() != null && !library.getCodeSystems().getDef().isEmpty()) ||
(library.getCodes() != null && library.getCodes().getDef() != null && !library.getCodes().getDef().isEmpty()) ||
(library.getValueSets() != null && library.getValueSets().getDef() != null && !library.getValueSets().getDef().isEmpty())) {
if (this.terminologyProvider == null) {
throw new IllegalArgumentException(String.format("Library %s has terminology requirements and no terminology provider is registered.",
this.getLibraryDescription(library.getIdentifier())));
}
}
}
private String getLibraryDescription(VersionedIdentifier libraryIdentifier) {
return libraryIdentifier.getId() + (libraryIdentifier.getVersion() != null ? ("-" + libraryIdentifier.getVersion()) : "");
}
private Map> getExpressionMap(Library library) {
Map> map = new HashMap<>();
Set expressionNames = new HashSet<>();
if (library.getStatements() != null && library.getStatements().getDef() != null) {
for (ExpressionDef ed : library.getStatements().getDef()) {
expressionNames.add(ed.getName());
}
}
map.put(library.getIdentifier(), expressionNames);
return map;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy