bonnie_bundler 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +12 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +267 -0
- data/README.md +4 -0
- data/Rakefile +29 -0
- data/bonnie-bundler.gemspec +29 -0
- data/config/initializers/mongo.rb +1 -0
- data/config/measures/measures_2_4_0.yml +719 -0
- data/config/mongoid.yml +6 -0
- data/lib/bonnie_bundler.rb +39 -0
- data/lib/ext/hash.rb +28 -0
- data/lib/ext/railtie.rb +11 -0
- data/lib/ext/valueset.rb +11 -0
- data/lib/measures/cql_to_elm_helper.rb +90 -0
- data/lib/measures/elm_parser.rb +74 -0
- data/lib/measures/loading/base_loader_definition.rb +61 -0
- data/lib/measures/loading/cql_loader.rb +420 -0
- data/lib/measures/loading/exceptions.rb +10 -0
- data/lib/measures/loading/loader.rb +178 -0
- data/lib/measures/loading/value_set_loader.rb +137 -0
- data/lib/measures/logic_extractor.rb +552 -0
- data/lib/measures/mongo_hash_key_wrapper.rb +44 -0
- data/lib/models/cql_measure.rb +160 -0
- data/lib/models/measure.rb +330 -0
- data/test/fixtures/BCS_v5_0_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts_Update.zip +0 -0
- data/test/fixtures/DRAFT_CMS2_CQL.zip +0 -0
- data/test/fixtures/bonnienesting01_fixed.zip +0 -0
- data/test/fixtures/vcr_cassettes/mat_5-4_cql_export_vsac_response.yml +4723 -0
- data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +1892 -0
- data/test/fixtures/vcr_cassettes/valid_translation_response.yml +1120 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +1678 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +3480 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_profile.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_verion.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_profile.yml +1196 -0
- data/test/fixtures/vcr_cassettes/vs_loading_version.yml +20331 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_Version.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_With_Profiles.zip +0 -0
- data/test/simplecov_init.rb +18 -0
- data/test/test_helper.rb +44 -0
- data/test/unit/load_mat_export_test.rb +181 -0
- data/test/unit/measure_complexity_test.rb +32 -0
- data/test/unit/measure_diff_test.rb +68 -0
- data/test/unit/mongo_hash_key_wrapper_test.rb +247 -0
- data/test/unit/storing_mat_export_package_test.rb +45 -0
- data/test/unit/value_set_loading_test.rb +109 -0
- data/test/vcr_setup.rb +20 -0
- metadata +258 -0
data/config/mongoid.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Top level include file that brings in all the necessary code
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'roo'
|
|
6
|
+
|
|
7
|
+
require 'quality-measure-engine'
|
|
8
|
+
require 'hqmf-parser'
|
|
9
|
+
require 'hqmf2js'
|
|
10
|
+
require 'simplexml_parser'
|
|
11
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
12
|
+
|
|
13
|
+
require_relative 'models/measure.rb'
|
|
14
|
+
require_relative 'models/cql_measure.rb'
|
|
15
|
+
require_relative 'measures/loading/exceptions.rb'
|
|
16
|
+
require_relative 'measures/loading/loader.rb'
|
|
17
|
+
require_relative 'measures/loading/base_loader_definition.rb'
|
|
18
|
+
require_relative 'measures/loading/cql_loader.rb'
|
|
19
|
+
require_relative 'measures/loading/value_set_loader.rb'
|
|
20
|
+
require_relative 'measures/logic_extractor.rb'
|
|
21
|
+
require_relative 'measures/mongo_hash_key_wrapper.rb'
|
|
22
|
+
require_relative 'ext/hash.rb'
|
|
23
|
+
require_relative 'ext/valueset.rb'
|
|
24
|
+
require_relative 'measures/elm_parser.rb'
|
|
25
|
+
require_relative 'measures/cql_to_elm_helper.rb'
|
|
26
|
+
require_relative '../config/initializers/mongo.rb'
|
|
27
|
+
|
|
28
|
+
module BonnieBundler
|
|
29
|
+
class << self
|
|
30
|
+
attr_accessor :logger
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if defined?(Rails)
|
|
35
|
+
require_relative 'ext/railtie'
|
|
36
|
+
else
|
|
37
|
+
BonnieBundler.logger = Log4r::Logger.new("Bonnie Bundler")
|
|
38
|
+
BonnieBundler.logger.outputters = Log4r::Outputter.stdout
|
|
39
|
+
end
|
data/lib/ext/hash.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
|
|
3
|
+
def remove_nils
|
|
4
|
+
clear_nils = Proc.new { |k, v| v.kind_of?(Hash) ? (v.delete_if(&clear_nils); nil) : v.nil? };
|
|
5
|
+
self.delete_if(&clear_nils)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def convert_keys_to_strings
|
|
9
|
+
Hash.convert_keys_to_strings(self)
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.convert_keys_to_strings(hash)
|
|
14
|
+
if hash.kind_of? Hash
|
|
15
|
+
hash.keys.each do |k|
|
|
16
|
+
v = hash[k]
|
|
17
|
+
if k.kind_of? Symbol
|
|
18
|
+
hash[k.to_s] = hash[k]
|
|
19
|
+
hash.delete(k)
|
|
20
|
+
end
|
|
21
|
+
Hash.convert_keys_to_strings(v)
|
|
22
|
+
end
|
|
23
|
+
elsif hash.kind_of? Array
|
|
24
|
+
hash.each{|val| Hash.convert_keys_to_strings(val)}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
data/lib/ext/railtie.rb
ADDED
data/lib/ext/valueset.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module HealthDataStandards
|
|
2
|
+
module SVS
|
|
3
|
+
class ValueSet
|
|
4
|
+
# include Mongoid::Document
|
|
5
|
+
belongs_to :user
|
|
6
|
+
belongs_to :bundle, class_name: "HealthDataStandards::CQM::Bundle"
|
|
7
|
+
scope :by_user, ->(user) { where({'user_id'=>(user ? user.id : nil)}) }
|
|
8
|
+
index "user_id" => 1
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module CqlElm
|
|
2
|
+
class CqlToElmHelper
|
|
3
|
+
# Translates the cql to elm json using a post request to CQLTranslation Jar.
|
|
4
|
+
# Returns an array of JSON ELM and an Array of XML ELM
|
|
5
|
+
def self.translate_cql_to_elm(cql)
|
|
6
|
+
begin
|
|
7
|
+
request = RestClient::Request.new(
|
|
8
|
+
:method => :post,
|
|
9
|
+
:accept => :json,
|
|
10
|
+
:content_type => :json,
|
|
11
|
+
:url => 'http://localhost:8080/cql/translator',
|
|
12
|
+
:payload => {
|
|
13
|
+
:multipart => true,
|
|
14
|
+
:file => cql
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
elm_json = request.execute
|
|
19
|
+
|
|
20
|
+
# now get the XML ELM
|
|
21
|
+
request = RestClient::Request.new(
|
|
22
|
+
:method => :post,
|
|
23
|
+
:headers => {
|
|
24
|
+
:accept => 'multipart/form-data',
|
|
25
|
+
'X-TargetFormat' => 'application/elm+xml'
|
|
26
|
+
},
|
|
27
|
+
:content_type => 'multipart/form-data',
|
|
28
|
+
:url => 'http://localhost:8080/cql/translator',
|
|
29
|
+
:payload => {
|
|
30
|
+
:multipart => true,
|
|
31
|
+
:file => cql
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
elm_xmls = request.execute
|
|
35
|
+
|
|
36
|
+
return parse_elm_response(elm_json), parse_multipart_response(elm_xmls)
|
|
37
|
+
rescue RestClient::BadRequest => e
|
|
38
|
+
begin
|
|
39
|
+
# If there is a response, include it in the error else just include the error message
|
|
40
|
+
cqlError = JSON.parse(e.response)
|
|
41
|
+
errorMsg = JSON.pretty_generate(cqlError).to_s
|
|
42
|
+
rescue
|
|
43
|
+
errorMsg = e.message
|
|
44
|
+
end
|
|
45
|
+
# The error text will be written to a load_error file and will not be displayed in the error dialog displayed to the user since
|
|
46
|
+
# measures_controller.rb does not handle this type of exception
|
|
47
|
+
raise MeasureLoadingException.new "Error Translating CQL to ELM: " + errorMsg
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
# Parse the JSON response into an array of json objects (one for each library)
|
|
53
|
+
def self.parse_elm_response(response)
|
|
54
|
+
parts = pre_process_response(response)
|
|
55
|
+
# Grabs everything from the first '{' to the last '}'
|
|
56
|
+
results = parts.map{ |part| part.match(/{.+}/m).to_s }
|
|
57
|
+
results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.parse_multipart_response(response)
|
|
61
|
+
parts = pre_process_response(response)
|
|
62
|
+
parsed_parts = []
|
|
63
|
+
parts.each do |part|
|
|
64
|
+
lines = part.split("\r\n")
|
|
65
|
+
# The first line will always be empty string
|
|
66
|
+
lines.shift
|
|
67
|
+
|
|
68
|
+
# find the end of the http headers
|
|
69
|
+
headerEndIndex = lines.find_index { |line| line == '' }
|
|
70
|
+
|
|
71
|
+
# Remove the headers and reassemble
|
|
72
|
+
lines.shift(headerEndIndex+1)
|
|
73
|
+
parsed_parts << lines.join("\r\n")
|
|
74
|
+
end
|
|
75
|
+
parsed_parts
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.pre_process_response(response)
|
|
79
|
+
# Not the same delimiter in the response as we specify ourselves in the request,
|
|
80
|
+
# so we have to extract it.
|
|
81
|
+
delimiter = response.split("\r\n")[0].strip
|
|
82
|
+
parts = response.split(delimiter)
|
|
83
|
+
# The first part will always be an empty string. Just remove it.
|
|
84
|
+
parts.shift
|
|
85
|
+
# The last part will be the "--". Just remove it.
|
|
86
|
+
parts.pop
|
|
87
|
+
parts
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module CqlElm
|
|
2
|
+
class Parser
|
|
3
|
+
#Fields are combined with the refId to find elm node that corrosponds to the current annotation node.
|
|
4
|
+
@fields = ['expression', 'operand', 'suchThat']
|
|
5
|
+
@previousNoTrailingSpaceNotPeriod = false
|
|
6
|
+
|
|
7
|
+
def self.parse(elm_xml)
|
|
8
|
+
ret = {
|
|
9
|
+
statements: [],
|
|
10
|
+
identifier: {}
|
|
11
|
+
}
|
|
12
|
+
@doc = Nokogiri::XML(elm_xml) {|d| d.noblanks}
|
|
13
|
+
#extract library identifier data
|
|
14
|
+
ret[:identifier][:id] = @doc.css("identifier").attr("id").value()
|
|
15
|
+
ret[:identifier][:version] = @doc.css("identifier").attr("version").value()
|
|
16
|
+
|
|
17
|
+
#extracts the fields of type "annotation" and their children.
|
|
18
|
+
annotations = @doc.css("annotation")
|
|
19
|
+
annotations.each do |node|
|
|
20
|
+
node, define_name = parse_node(node)
|
|
21
|
+
if !define_name.nil?
|
|
22
|
+
node[:define_name] = define_name
|
|
23
|
+
ret[:statements] << node
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
ret
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#Recursive function that traverses the annotation tree and constructs a representation
|
|
30
|
+
#that will be compatible with the front end.
|
|
31
|
+
def self.parse_node(node)
|
|
32
|
+
ret = {
|
|
33
|
+
children: []
|
|
34
|
+
}
|
|
35
|
+
define_name = nil
|
|
36
|
+
node.children.each do |child|
|
|
37
|
+
#Nodes with the 'a' prefix are not leaf nodes
|
|
38
|
+
if child.namespace.respond_to?(:prefix) && child.namespace.prefix == 'a'
|
|
39
|
+
node_type = extract_node_type(child)
|
|
40
|
+
#Parses the current child recursively. child_define_name will bubble up to indicate which
|
|
41
|
+
#statement is currently being traversed.
|
|
42
|
+
node, child_define_name = parse_node(child)
|
|
43
|
+
node[:node_type] = node_type unless node_type.nil?
|
|
44
|
+
node[:ref_id] = child['r'] unless child['r'].nil?
|
|
45
|
+
define_name = child_define_name unless child_define_name.nil?
|
|
46
|
+
ret[:children] << node
|
|
47
|
+
else
|
|
48
|
+
if (/^define/ =~ child.to_html)
|
|
49
|
+
define_name = child.to_html.split("\"")[1]
|
|
50
|
+
end
|
|
51
|
+
clause = {
|
|
52
|
+
text: child.to_html.gsub(/\t/, " ")
|
|
53
|
+
}
|
|
54
|
+
clause[:ref_id] = child['r'] unless child['r'].nil?
|
|
55
|
+
ret[:children] << clause
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return ret, define_name
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.extract_node_type(child)
|
|
62
|
+
ref_node = nil
|
|
63
|
+
node_type = nil
|
|
64
|
+
#Tries to pair the current annotation node with an elm node.
|
|
65
|
+
@fields.each do |field|
|
|
66
|
+
ref_node ||= @doc.at_css(field + '[localId="'+child['r']+'"]') unless child['r'].nil?
|
|
67
|
+
end
|
|
68
|
+
#Tries to extract the current node's type.
|
|
69
|
+
node_type = ref_node['xsi:type'] unless ref_node.nil?
|
|
70
|
+
node_type
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Measures
|
|
2
|
+
# Base Class for the different types of loader formats Bonnie Bundler supports.
|
|
3
|
+
class BaseLoaderDefinition
|
|
4
|
+
|
|
5
|
+
def self.extract(zip_file, entry, out_dir)
|
|
6
|
+
out_file = File.join(out_dir,Pathname.new(entry.name).basename.to_s)
|
|
7
|
+
zip_file.extract(entry, out_file)
|
|
8
|
+
out_file
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Wrapper function that performs checks before extracting all xml files in the given zip
|
|
12
|
+
# Returns a hash with the type of xml files present and their paths.
|
|
13
|
+
# Ex: {:HQMF_XML => '/var/149825825jf/Test111_eMeasure.xml'}
|
|
14
|
+
def self.extract_xml_files(zip_file, files, output_directory=nil)
|
|
15
|
+
file_paths_hash = {}
|
|
16
|
+
if files.count > 0
|
|
17
|
+
# If no directory is given, create a new temporary directory.
|
|
18
|
+
if output_directory.nil?
|
|
19
|
+
# Create a temporary directory to extract all xml files contained in the zip.
|
|
20
|
+
Dir.mktmpdir do |dir|
|
|
21
|
+
file_paths_hash = extract_to_temporary_location(zip_file, files, dir)
|
|
22
|
+
end
|
|
23
|
+
# Use the provided directory to extract the files to.
|
|
24
|
+
else
|
|
25
|
+
file_paths_hash = extract_to_temporary_location(zip_file, files, output_directory)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
file_paths_hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Extracts the xml files from the zip and provides a key value pair for HQMF and ELM xml files.
|
|
34
|
+
# Currently only checks for HQMF xml, ELM xml and SIMPLE xml. Uses the root node in each of the files.
|
|
35
|
+
# {file_type => file_path}
|
|
36
|
+
def self.extract_to_temporary_location(zip_file, files, output_directory)
|
|
37
|
+
file_paths_hash = {}
|
|
38
|
+
file_paths_hash[:ELM_XML] = []
|
|
39
|
+
begin
|
|
40
|
+
# Iterate over all files passed in, extract file to temporary directory.
|
|
41
|
+
files.each do |xml_file|
|
|
42
|
+
if xml_file && xml_file.size > 0
|
|
43
|
+
xml_file_path = extract(zip_file, xml_file, output_directory)
|
|
44
|
+
# Open up xml file and read contents.
|
|
45
|
+
doc = Nokogiri::XML.parse(File.read(xml_file_path))
|
|
46
|
+
# Check if root node in xml file matches either the HQMF file or ELM file.
|
|
47
|
+
if doc.root.name == 'QualityMeasureDocument' # Root node for HQMF XML
|
|
48
|
+
file_paths_hash[:HQMF_XML] = xml_file_path
|
|
49
|
+
elsif doc.root.name == 'library' # Root node for ELM XML
|
|
50
|
+
file_paths_hash[:ELM_XML] << xml_file_path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
rescue Exception => e
|
|
55
|
+
raise MeasureLoadingException.new "Error Checking MAT Export: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
file_paths_hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
module Measures
|
|
2
|
+
# Utility class for loading CQL measure definitions into the database from the MAT export zip
|
|
3
|
+
class CqlLoader < BaseLoaderDefinition
|
|
4
|
+
|
|
5
|
+
def self.mat_cql_export?(zip_file)
|
|
6
|
+
# Open the zip file and iterate over each of the files.
|
|
7
|
+
Zip::ZipFile.open(zip_file.path) do |zip_file|
|
|
8
|
+
# Check for CQL, HQMF, ELM and Human Readable
|
|
9
|
+
cql_entry = zip_file.glob(File.join('**','**.cql')).select {|x| !x.name.starts_with?('__MACOSX') }.first
|
|
10
|
+
elm_json = zip_file.glob(File.join('**','**.json')).select {|x| !x.name.starts_with?('__MACOSX') }.first
|
|
11
|
+
human_readable_entry = zip_file.glob(File.join('**','**.html')).select { |x| !x.name.starts_with?('__MACOSX') }.first
|
|
12
|
+
|
|
13
|
+
# Grab all xml files in the zip.
|
|
14
|
+
zip_xml_files = zip_file.glob(File.join('**','**.xml')).select {|x| !x.name.starts_with?('__MACOSX') }
|
|
15
|
+
|
|
16
|
+
if zip_xml_files.count > 0
|
|
17
|
+
xml_files_hash = extract_xml_files(zip_file, zip_xml_files)
|
|
18
|
+
!cql_entry.nil? && !elm_json.nil? && !human_readable_entry.nil? && !xml_files_hash[:HQMF_XML].nil? && !xml_files_hash[:ELM_XML].nil?
|
|
19
|
+
else
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.load_mat_cql_exports(user, zip_file, out_dir, measure_details, vsac_user, vsac_password, overwrite_valuesets=false, cache=false, includeDraft=false, ticket_granting_ticket=nil)
|
|
26
|
+
measure = nil
|
|
27
|
+
cql = nil
|
|
28
|
+
hqmf_path = nil
|
|
29
|
+
# Grabs the cql file contents, the elm_xml contents, elm_json contents and the hqmf file path
|
|
30
|
+
files = get_files_from_zip(zip_file, out_dir)
|
|
31
|
+
|
|
32
|
+
# Load hqmf into HQMF Parser
|
|
33
|
+
hqmf_model = Measures::Loader.parse_hqmf_model(files[:HQMF_XML_PATH])
|
|
34
|
+
|
|
35
|
+
# Get main measure from hqmf parser
|
|
36
|
+
main_cql_library = hqmf_model.cql_measure_library
|
|
37
|
+
|
|
38
|
+
cql_artifacts = process_cql(files, main_cql_library, user, vsac_user, vsac_password, overwrite_valuesets, cache, includeDraft, ticket_granting_ticket, hqmf_model.hqmf_set_id)
|
|
39
|
+
|
|
40
|
+
# Create CQL Measure
|
|
41
|
+
hqmf_model.backfill_patient_characteristics_with_codes(cql_artifacts[:all_codes_and_code_names])
|
|
42
|
+
json = hqmf_model.to_json
|
|
43
|
+
json.convert_keys_to_strings
|
|
44
|
+
|
|
45
|
+
# Loop over data criteria to search for data criteria that is using a single reference code.
|
|
46
|
+
# Once found set the Data Criteria's 'code_list_id' to our fake oid. Do the same for source data criteria.
|
|
47
|
+
json['data_criteria'].each do |data_criteria_name, data_criteria|
|
|
48
|
+
# We do not want to replace an existing code_list_id. Skip.
|
|
49
|
+
unless data_criteria['code_list_id']
|
|
50
|
+
if data_criteria['inline_code_list']
|
|
51
|
+
# Check to see if inline_code_list contains the correct code_system and code for a direct reference code.
|
|
52
|
+
data_criteria['inline_code_list'].each do |code_system, code_list|
|
|
53
|
+
# Loop over all single code reference objects.
|
|
54
|
+
cql_artifacts[:single_code_references].each do |single_code_object|
|
|
55
|
+
# If Data Criteria contains a matching code system, check if the correct code exists in the data critera values.
|
|
56
|
+
# If both values match, set the Data Criteria's 'code_list_id' to the single_code_object_guid.
|
|
57
|
+
if code_system == single_code_object[:code_system_name] && code_list.include?(single_code_object[:code])
|
|
58
|
+
data_criteria['code_list_id'] = single_code_object[:guid]
|
|
59
|
+
# Modify the matching source data criteria
|
|
60
|
+
json['source_data_criteria'][data_criteria_name + "_source"]['code_list_id'] = single_code_object[:guid]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Create CQL Measure
|
|
69
|
+
measure = Measures::Loader.load_hqmf_cql_model_json(json, user, cql_artifacts[:all_value_set_oids], main_cql_library, cql_artifacts[:cql_definition_dependency_structure],
|
|
70
|
+
cql_artifacts[:elms], cql_artifacts[:elm_annotations], files[:CQL], nil, cql_artifacts[:value_set_oid_version_objects])
|
|
71
|
+
measure['episode_of_care'] = measure_details['episode_of_care']
|
|
72
|
+
measure['type'] = measure_details['type']
|
|
73
|
+
|
|
74
|
+
# Create, associate and save the measure package.
|
|
75
|
+
measure.package = CqlMeasurePackage.new(file: BSON::Binary.new(zip_file.read()))
|
|
76
|
+
measure.package.save
|
|
77
|
+
|
|
78
|
+
measure
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.load(file, user, measure_details, vsac_user=nil, vsac_password=nil, overwrite_valuesets=false, cache=false, includeDraft=false, ticket_granting_ticket=nil)
|
|
82
|
+
measure = nil
|
|
83
|
+
Dir.mktmpdir do |dir|
|
|
84
|
+
measure = load_mat_cql_exports(user, file, dir, measure_details, vsac_user, vsac_password, overwrite_valuesets, cache, includeDraft, ticket_granting_ticket)
|
|
85
|
+
end
|
|
86
|
+
measure
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Manages all of the CQL processing that is not related to the HQMF.
|
|
90
|
+
def self.process_cql(files, main_cql_library, user, vsac_user=nil, vsac_password=nil, overwrite_valuesets=nil, cache=nil, includeDraft=nil, ticket_granting_ticket=nil, measure_id=nil)
|
|
91
|
+
elm_strings = files[:ELM_JSON]
|
|
92
|
+
# Removes 'urn:oid:' from ELM for Bonnie and Parse the JSON
|
|
93
|
+
elm_strings.each { |elm_string| elm_string.gsub! 'urn:oid:', '' }
|
|
94
|
+
elms = elm_strings.map{ |elm| JSON.parse(elm, :max_nesting=>1000)}
|
|
95
|
+
elm_annotations = parse_elm_annotations(files[:ELM_XML])
|
|
96
|
+
|
|
97
|
+
# Hash of define statements to which define statements they use.
|
|
98
|
+
cql_definition_dependency_structure = populate_cql_definition_dependency_structure(main_cql_library, elms)
|
|
99
|
+
# Go back for the library statements
|
|
100
|
+
cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
|
|
101
|
+
|
|
102
|
+
# fix up statement names in cql_statement_dependencies to not use periods
|
|
103
|
+
Measures::MongoHashKeyWrapper::wrapKeys cql_definition_dependency_structure
|
|
104
|
+
|
|
105
|
+
# Depening on the value of the value set version, change it to null, strip out a substring or leave it alone.
|
|
106
|
+
modify_value_set_versions(elms)
|
|
107
|
+
|
|
108
|
+
# Grab the value sets from the elm
|
|
109
|
+
elm_value_sets = []
|
|
110
|
+
elms.each do | elm |
|
|
111
|
+
# Confirm the library has value sets
|
|
112
|
+
if elm['library'] && elm['library']['valueSets'] && elm['library']['valueSets']['def']
|
|
113
|
+
elm['library']['valueSets']['def'].each do |value_set|
|
|
114
|
+
elm_value_sets << {oid: value_set['id'], version: value_set['version'], profile: value_set['profile']}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
# Get Value Sets
|
|
119
|
+
value_set_models = []
|
|
120
|
+
if (vsac_user && vsac_password) || ticket_granting_ticket
|
|
121
|
+
begin
|
|
122
|
+
value_set_models = Measures::ValueSetLoader.load_value_sets_from_vsac(elm_value_sets, vsac_user, vsac_password, user, overwrite_valuesets, includeDraft, ticket_granting_ticket, cache, measure_id)
|
|
123
|
+
rescue Exception => e
|
|
124
|
+
raise VSACException.new "Error Loading Value Sets from VSAC: #{e.message}"
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
# if VSAC credentials aren't provided, find the value sets in the database
|
|
128
|
+
elm_value_sets.each do |elm_value_set|
|
|
129
|
+
version = elm_value_set[:version] || "N/A" # 'N/A' is what is stored in the DB for value sets without versions
|
|
130
|
+
query_params = {user_id: user.id, oid: elm_value_set[:oid]}
|
|
131
|
+
|
|
132
|
+
if (elm_value_set[:profile])
|
|
133
|
+
query_params[:profile] = elm_value_set[:profile]
|
|
134
|
+
else
|
|
135
|
+
query_params[:version] = version
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
value_set = HealthDataStandards::SVS::ValueSet.where(query_params).first()
|
|
139
|
+
if value_set
|
|
140
|
+
value_set_models << value_set
|
|
141
|
+
elsif version == "N/A"
|
|
142
|
+
# if the version is "N/A" and a value set doesn't exist with that version, just grab the existing value set
|
|
143
|
+
value_set = HealthDataStandards::SVS::ValueSet.where({user_id: user.id, oid: elm_value_set[:oid]}).first()
|
|
144
|
+
if value_set
|
|
145
|
+
value_set_models << value_set
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Get code systems and codes for all value sets in the elm.
|
|
153
|
+
all_codes_and_code_names = HQMF2JS::Generator::CodesToJson.from_value_sets(value_set_models)
|
|
154
|
+
# Replace code system oids with friendly names
|
|
155
|
+
# TODO: preferred solution would be to continue using OIDs in the ELM and enable Bonnie to supply those OIDs
|
|
156
|
+
# to the calculation engine in patient data and value sets.
|
|
157
|
+
replace_codesystem_oids_with_names(elms)
|
|
158
|
+
|
|
159
|
+
# Generate single reference code objects and a complete list of code systems and codes for the measure.
|
|
160
|
+
single_code_references, all_codes_and_code_names = generate_single_code_references(elms, all_codes_and_code_names, user)
|
|
161
|
+
|
|
162
|
+
# Add our new fake oids to measure value sets.
|
|
163
|
+
all_value_set_oids = value_set_models.collect{|vs| vs.oid}
|
|
164
|
+
single_code_references.each do |single_code|
|
|
165
|
+
all_value_set_oids << single_code[:guid]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Add a list of value set oids and their versions
|
|
169
|
+
value_set_oid_version_objects = get_value_set_oid_version_objects(value_set_models, single_code_references)
|
|
170
|
+
|
|
171
|
+
cql_artifacts = {:elms => elms,
|
|
172
|
+
:elm_annotations => elm_annotations,
|
|
173
|
+
:cql_definition_dependency_structure => cql_definition_dependency_structure,
|
|
174
|
+
:all_value_set_oids => all_value_set_oids,
|
|
175
|
+
:value_set_oid_version_objects => value_set_oid_version_objects,
|
|
176
|
+
:single_code_references => single_code_references,
|
|
177
|
+
:all_codes_and_code_names => all_codes_and_code_names}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# returns a list of objects that include the valueset oids and their versions
|
|
181
|
+
def self.get_value_set_oid_version_objects(value_sets, single_code_references)
|
|
182
|
+
# [LDC] need to make this an array of objects instead of a hash because Mongo is
|
|
183
|
+
# dumb and *let's you* have dots in keys on object creation but *doesn't let you*
|
|
184
|
+
# have dots in keys on object update or retrieve....
|
|
185
|
+
value_set_oid_version_objects = []
|
|
186
|
+
value_sets.each do |vs|
|
|
187
|
+
value_set_oid_version_objects << {:oid => vs.oid, :version => vs.version}
|
|
188
|
+
end
|
|
189
|
+
single_code_references.each do |single_code|
|
|
190
|
+
value_set_oid_version_objects << {:oid => single_code[:guid], :version => ""}
|
|
191
|
+
end
|
|
192
|
+
value_set_oid_version_objects
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Replace all the code system ids that are oids with the friendly name of the code system
|
|
196
|
+
# TODO: preferred solution would be to continue using OIDs in the ELM and enable Bonnie to supply those OIDs
|
|
197
|
+
# to the calculation engine in patient data and value sets.
|
|
198
|
+
def self.replace_codesystem_oids_with_names(elms)
|
|
199
|
+
elms.each do |elm|
|
|
200
|
+
# Only do replacement if there are any code systems in this library.
|
|
201
|
+
if elm['library'].has_key?('codeSystems')
|
|
202
|
+
elm['library']['codeSystems']['def'].each do |code_system|
|
|
203
|
+
code_name = HealthDataStandards::Util::CodeSystemHelper.code_system_for(code_system['id'])
|
|
204
|
+
# if the helper returns "Unknown" then keep what was there
|
|
205
|
+
code_system['id'] = code_name unless code_name == "Unknown"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Adjusting value set version data. If version is profile, set the version to nil
|
|
212
|
+
def self.modify_value_set_versions(elms)
|
|
213
|
+
elms.each do |elm|
|
|
214
|
+
if elm['library']['valueSets'] && elm['library']['valueSets']['def']
|
|
215
|
+
elm['library']['valueSets']['def'].each do |value_set|
|
|
216
|
+
# If value set has a version and it starts with 'urn:hl7:profile:' then set to nil
|
|
217
|
+
if value_set['version'] && value_set['version'].include?('urn:hl7:profile:')
|
|
218
|
+
value_set['profile'] = URI.decode(value_set['version'].split('urn:hl7:profile:').last)
|
|
219
|
+
value_set['version'] = nil
|
|
220
|
+
# If value has a version and it starts with 'urn:hl7:version:' then strip that and keep the actual version value.
|
|
221
|
+
elsif value_set['version'] && value_set['version'].include?('urn:hl7:version:')
|
|
222
|
+
value_set['version'] = URI.decode(value_set['version'].split('urn:hl7:version:').last)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
# Add single code references by finding the codes from the elm and creating new ValueSet objects
|
|
229
|
+
# With a generated GUID as a fake oid.
|
|
230
|
+
def self.generate_single_code_references(elms, all_codes_and_code_names, user)
|
|
231
|
+
single_code_references = []
|
|
232
|
+
# Add all single code references from each elm file
|
|
233
|
+
elms.each do | elm |
|
|
234
|
+
# Check if elm has single reference code.
|
|
235
|
+
if elm['library'] && elm['library']['codes'] && elm['library']['codes']['def']
|
|
236
|
+
# Loops over all single codes and saves them as fake valuesets.
|
|
237
|
+
elm['library']['codes']['def'].each do |code_reference|
|
|
238
|
+
code_sets = {}
|
|
239
|
+
|
|
240
|
+
# look up the referenced code system
|
|
241
|
+
code_system_def = elm['library']['codeSystems']['def'].find { |code_sys| code_sys['name'] == code_reference['codeSystem']['name'] }
|
|
242
|
+
|
|
243
|
+
code_system_name = code_system_def['id']
|
|
244
|
+
code_system_version = code_system_def['version']
|
|
245
|
+
|
|
246
|
+
code_sets[code_system_name] ||= []
|
|
247
|
+
code_sets[code_system_name] << code_reference['id']
|
|
248
|
+
# Generate a unique number as our fake "oid"
|
|
249
|
+
code_guid = SecureRandom.uuid
|
|
250
|
+
# Keep a list of generated_guids and a hash of guids with code system names and codes.
|
|
251
|
+
single_code_references << { guid: code_guid, code_system_name: code_system_name, code: code_reference['id'] }
|
|
252
|
+
|
|
253
|
+
all_codes_and_code_names[code_guid] = code_sets
|
|
254
|
+
# Create a new "ValueSet" and "Concept" object and save.
|
|
255
|
+
valueSet = HealthDataStandards::SVS::ValueSet.new({oid: code_guid, display_name: code_reference['name'], version: '' ,concepts: [], user_id: user.id})
|
|
256
|
+
concept = HealthDataStandards::SVS::Concept.new({code: code_reference['id'], code_system_name: code_system_name, code_system_version: code_system_version, display_name: code_reference['name']})
|
|
257
|
+
valueSet.concepts << concept
|
|
258
|
+
valueSet.save!
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
# Returns a list of single code objects and a complete list of code systems and codes for all valuesets on the measure.
|
|
263
|
+
return single_code_references, all_codes_and_code_names
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Opens the zip and grabs the cql file contents, the ELM contents (XML and JSON) and hqmf_path.
|
|
267
|
+
def self.get_files_from_zip(zip_file, out_dir)
|
|
268
|
+
Zip::ZipFile.open(zip_file.path) do |file|
|
|
269
|
+
cql_entries = file.glob(File.join('**','**.cql')).select {|x| !x.name.starts_with?('__MACOSX') }
|
|
270
|
+
zip_xml_files = file.glob(File.join('**','**.xml')).select {|x| !x.name.starts_with?('__MACOSX') }
|
|
271
|
+
elm_json_entries = file.glob(File.join('**','**.json')).select {|x| !x.name.starts_with?('__MACOSX') }
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
cql_paths = []
|
|
275
|
+
cql_entries.each do |cql_file|
|
|
276
|
+
cql_paths << extract(file, cql_file, out_dir) if cql_file.size > 0
|
|
277
|
+
end
|
|
278
|
+
cql_contents = []
|
|
279
|
+
cql_paths.each do |cql_path|
|
|
280
|
+
cql_contents << open(cql_path).read
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
elm_json_paths = []
|
|
284
|
+
elm_json_entries.each do |json_file|
|
|
285
|
+
elm_json_paths << extract(file, json_file, out_dir) if json_file.size > 0
|
|
286
|
+
end
|
|
287
|
+
elm_json = []
|
|
288
|
+
elm_json_paths.each do |elm_json_path|
|
|
289
|
+
elm_json << open(elm_json_path).read
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
xml_file_paths = extract_xml_files(file, zip_xml_files, out_dir)
|
|
293
|
+
elm_xml_paths = xml_file_paths[:ELM_XML]
|
|
294
|
+
elm_xml = []
|
|
295
|
+
elm_xml_paths.each do |elm_xml_path|
|
|
296
|
+
elm_xml << open(elm_xml_path).read
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
files = { :HQMF_XML_PATH => xml_file_paths[:HQMF_XML],
|
|
300
|
+
:ELM_JSON => elm_json,
|
|
301
|
+
:CQL => cql_contents,
|
|
302
|
+
:ELM_XML => elm_xml }
|
|
303
|
+
return files
|
|
304
|
+
rescue Exception => e
|
|
305
|
+
raise MeasureLoadingException.new "Error Parsing Measure Logic: #{e.message}"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
def self.parse_elm_annotations(xmls)
|
|
312
|
+
elm_annotations = {}
|
|
313
|
+
xmls.each do |xml_lib|
|
|
314
|
+
lib_annotations = CqlElm::Parser.parse(xml_lib)
|
|
315
|
+
elm_annotations[lib_annotations[:identifier][:id]] = lib_annotations
|
|
316
|
+
end
|
|
317
|
+
elm_annotations
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Loops over the populations and retrieves the define statements that are nested within it.
|
|
321
|
+
def self.populate_cql_definition_dependency_structure(main_cql_library, elms)
|
|
322
|
+
cql_statement_depencency_map = {}
|
|
323
|
+
main_library_elm = elms.find { |elm| elm['library']['identifier']['id'] == main_cql_library }
|
|
324
|
+
|
|
325
|
+
cql_statement_depencency_map[main_cql_library] = {}
|
|
326
|
+
main_library_elm['library']['statements']['def'].each { |statement|
|
|
327
|
+
cql_statement_depencency_map[main_cql_library][statement['name']] = retrieve_all_statements_in_population(statement, elms)
|
|
328
|
+
}
|
|
329
|
+
cql_statement_depencency_map
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Given a starting define statement, a starting library and all of the libraries,
|
|
333
|
+
# this will return an array of all nested define statements.
|
|
334
|
+
def self.retrieve_all_statements_in_population(statement, elms)
|
|
335
|
+
all_results = []
|
|
336
|
+
if statement.is_a? String
|
|
337
|
+
statement = retrieve_sub_statement_for_expression_name(statement, elms)
|
|
338
|
+
end
|
|
339
|
+
sub_statement_names = retrieve_expressions_from_statement(statement)
|
|
340
|
+
# Currently if sub_statement_name is another Population we do not remove it.
|
|
341
|
+
if sub_statement_names.length > 0
|
|
342
|
+
sub_statement_names.each do |sub_statement_name|
|
|
343
|
+
# Check if the statement is not a built in expression
|
|
344
|
+
sub_library_name, sub_statement = retrieve_sub_statement_for_expression_name(sub_statement_name, elms)
|
|
345
|
+
if sub_statement
|
|
346
|
+
all_results << { library_name: sub_library_name, statement_name: sub_statement_name }
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
all_results
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Finds which library the given define statement exists in.
|
|
354
|
+
# Returns the JSON statement that contains the given name.
|
|
355
|
+
# If given statement name is a built in expression, return nil.
|
|
356
|
+
def self.retrieve_sub_statement_for_expression_name(name, elms)
|
|
357
|
+
elms.each do | parsed_elm |
|
|
358
|
+
parsed_elm['library']['statements']['def'].each do |statement|
|
|
359
|
+
return [parsed_elm['library']['identifier']['id'], statement] if statement['name'] == name
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Traverses the given statement and returns all of the potential additional statements.
|
|
366
|
+
def self.retrieve_expressions_from_statement(statement)
|
|
367
|
+
expressions = []
|
|
368
|
+
statement.each do |k, v|
|
|
369
|
+
# If v is nil, an array is being iterated and the value is k.
|
|
370
|
+
# If v is not nil, a hash is being iterated and the value is v.
|
|
371
|
+
value = v || k
|
|
372
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
|
373
|
+
expressions.concat(retrieve_expressions_from_statement(value))
|
|
374
|
+
else
|
|
375
|
+
if k == 'type' && (v == 'ExpressionRef' || v == 'FunctionRef')
|
|
376
|
+
# We ignore the Patient expression because it isn't an actual define statment in the cql
|
|
377
|
+
expressions << statement['name'] unless statement['name'] == 'Patient'
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
expressions
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Loops over keys of the given hash and loops over the list of statements
|
|
385
|
+
# Original structure of hash is {IPP => ["In Demographics", Measurement Period Encounters"], NUMER => ["Tonsillitis"]}
|
|
386
|
+
def self.populate_used_library_dependencies(starting_hash, main_cql_library, elms)
|
|
387
|
+
# Starting_hash gets updated with the create_hash_for_all call.
|
|
388
|
+
starting_hash[main_cql_library].keys.each do |key|
|
|
389
|
+
starting_hash[main_cql_library][key].each do |statement|
|
|
390
|
+
create_hash_for_all(starting_hash, statement, elms)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
starting_hash
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Traverse list, create keys and drill down for each key.
|
|
397
|
+
# If key is already in place, skip.
|
|
398
|
+
def self.create_hash_for_all(starting_hash, key_statement, elms)
|
|
399
|
+
# If key already exists, return hash
|
|
400
|
+
if (starting_hash.has_key?(key_statement[:library_name]) &&
|
|
401
|
+
starting_hash[key_statement[:library_name]].has_key?(key_statement[:statement_name]))
|
|
402
|
+
return starting_hash
|
|
403
|
+
# Create new hash key and retrieve all sub statements
|
|
404
|
+
else
|
|
405
|
+
# create library hash key if needed
|
|
406
|
+
if !starting_hash.has_key?(key_statement[:library_name])
|
|
407
|
+
starting_hash[key_statement[:library_name]] = {}
|
|
408
|
+
end
|
|
409
|
+
starting_hash[key_statement[:library_name]][key_statement[:statement_name]] = retrieve_all_statements_in_population(key_statement[:statement_name], elms).uniq
|
|
410
|
+
# If there are no statements return hash
|
|
411
|
+
return starting_hash if starting_hash[key_statement[:library_name]][key_statement[:statement_name]].empty?
|
|
412
|
+
# Loop over array of sub statements and build out hash keys for each.
|
|
413
|
+
starting_hash[key_statement[:library_name]][key_statement[:statement_name]].each do |statement|
|
|
414
|
+
starting_hash.merge!(create_hash_for_all(starting_hash, statement, elms))
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
starting_hash
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|