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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +12 -0
  4. data/Gemfile +29 -0
  5. data/Gemfile.lock +267 -0
  6. data/README.md +4 -0
  7. data/Rakefile +29 -0
  8. data/bonnie-bundler.gemspec +29 -0
  9. data/config/initializers/mongo.rb +1 -0
  10. data/config/measures/measures_2_4_0.yml +719 -0
  11. data/config/mongoid.yml +6 -0
  12. data/lib/bonnie_bundler.rb +39 -0
  13. data/lib/ext/hash.rb +28 -0
  14. data/lib/ext/railtie.rb +11 -0
  15. data/lib/ext/valueset.rb +11 -0
  16. data/lib/measures/cql_to_elm_helper.rb +90 -0
  17. data/lib/measures/elm_parser.rb +74 -0
  18. data/lib/measures/loading/base_loader_definition.rb +61 -0
  19. data/lib/measures/loading/cql_loader.rb +420 -0
  20. data/lib/measures/loading/exceptions.rb +10 -0
  21. data/lib/measures/loading/loader.rb +178 -0
  22. data/lib/measures/loading/value_set_loader.rb +137 -0
  23. data/lib/measures/logic_extractor.rb +552 -0
  24. data/lib/measures/mongo_hash_key_wrapper.rb +44 -0
  25. data/lib/models/cql_measure.rb +160 -0
  26. data/lib/models/measure.rb +330 -0
  27. data/test/fixtures/BCS_v5_0_Artifacts.zip +0 -0
  28. data/test/fixtures/CMS158_v5_4_Artifacts.zip +0 -0
  29. data/test/fixtures/CMS158_v5_4_Artifacts_Update.zip +0 -0
  30. data/test/fixtures/DRAFT_CMS2_CQL.zip +0 -0
  31. data/test/fixtures/bonnienesting01_fixed.zip +0 -0
  32. data/test/fixtures/vcr_cassettes/mat_5-4_cql_export_vsac_response.yml +4723 -0
  33. data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +1892 -0
  34. data/test/fixtures/vcr_cassettes/valid_translation_response.yml +1120 -0
  35. data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +1678 -0
  36. data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +1670 -0
  37. data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +1670 -0
  38. data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +3480 -0
  39. data/test/fixtures/vcr_cassettes/vs_loading_draft_no_profile_version.yml +1198 -0
  40. data/test/fixtures/vcr_cassettes/vs_loading_draft_profile.yml +1198 -0
  41. data/test/fixtures/vcr_cassettes/vs_loading_draft_verion.yml +1198 -0
  42. data/test/fixtures/vcr_cassettes/vs_loading_no_profile_version.yml +1198 -0
  43. data/test/fixtures/vcr_cassettes/vs_loading_profile.yml +1196 -0
  44. data/test/fixtures/vcr_cassettes/vs_loading_version.yml +20331 -0
  45. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts.zip +0 -0
  46. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_Version.zip +0 -0
  47. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_With_Profiles.zip +0 -0
  48. data/test/simplecov_init.rb +18 -0
  49. data/test/test_helper.rb +44 -0
  50. data/test/unit/load_mat_export_test.rb +181 -0
  51. data/test/unit/measure_complexity_test.rb +32 -0
  52. data/test/unit/measure_diff_test.rb +68 -0
  53. data/test/unit/mongo_hash_key_wrapper_test.rb +247 -0
  54. data/test/unit/storing_mat_export_package_test.rb +45 -0
  55. data/test/unit/value_set_loading_test.rb +109 -0
  56. data/test/vcr_setup.rb +20 -0
  57. metadata +258 -0
@@ -0,0 +1,6 @@
1
+ test:
2
+ clients:
3
+ default:
4
+ database: bonnie_bundler_test
5
+ hosts:
6
+ - localhost:27017
@@ -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
@@ -0,0 +1,11 @@
1
+ module BonnieBundler
2
+ class Railtie < Rails::Railtie
3
+ initializer 'Rails logger' do
4
+ BonnieBundler.logger = Rails.logger
5
+ end
6
+ rake_tasks do
7
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
8
+ end
9
+
10
+ end
11
+ end
@@ -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