lib/measures/loading/cql_loader.rb in bonnie_bundler-2.2.3 vs lib/measures/loading/cql_loader.rb in bonnie_bundler-2.2.4
- old
+ new
@@ -1,64 +1,95 @@
module Measures
# Utility class for loading CQL measure definitions into the database from the MAT export zip
- class CqlLoader < BaseLoaderDefinition
+ class CqlLoader
+ # Returns true if ths uploaded measure zip file is a composite measure
+ def self.composite_measure?(measure_dir)
+ # Look through all xml files at current directory level and find QDM
+ files = Dir.glob("#{measure_dir}/**.xml").select
+ begin
+ # Iterate over all files passed in, extract file to temporary directory.
+ files.each do |xml_file|
+ if xml_file && xml_file.size > 0
+ # Open up xml file and read contents.
+ doc = Nokogiri::XML.parse(File.read(xml_file))
+ # Check if root node in xml file matches either the HQMF file or ELM file.
+ if doc.root.name == 'QualityMeasureDocument' # Root node for HQMF XML
+ # Xpath to determine if it is a composite or not
+ doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
+ return !doc.at_xpath('//cda:measureAttribute[cda:code[@code="MSRTYPE"]][cda:value[@code="COMPOSITE"]]').nil?
+ end
+ end
+ end
+ rescue Exception => e
+ raise MeasureLoadingException.new "Error Checking MAT Export: #{e.message}"
+ end
+ false
+ end
+ # Verifies that the zip file contains a valid measure
+ # Works for both regular & composite measures
def self.mat_cql_export?(zip_file)
- # Open the zip file and iterate over each of the files.
- Zip::ZipFile.open(zip_file.path) do |zip_file|
- # Check for CQL, HQMF, ELM and Human Readable
- cql_entry = zip_file.glob(File.join('**','**.cql')).select {|x| !x.name.starts_with?('__MACOSX') }.first
- elm_json = zip_file.glob(File.join('**','**.json')).select {|x| !x.name.starts_with?('__MACOSX') }.first
- human_readable_entry = zip_file.glob(File.join('**','**.html')).select { |x| !x.name.starts_with?('__MACOSX') }.first
+ # Extract contents of zip file while retaining the directory structure
+ original = Dir.pwd
+ Dir.mktmpdir do |tmp_dir|
+ current_directory = unzip_measure_contents(zip_file, tmp_dir)
+ # Check if measure contents are valid
+ return valid_measure_contents?(current_directory, true)
+ end
+ end
- # Grab all xml files in the zip.
- zip_xml_files = zip_file.glob(File.join('**','**.xml')).select {|x| !x.name.starts_with?('__MACOSX') }
-
- if zip_xml_files.count > 0
- xml_files_hash = extract_xml_files(zip_file, zip_xml_files)
- !cql_entry.nil? && !elm_json.nil? && !human_readable_entry.nil? && !xml_files_hash[:HQMF_XML].nil? && !xml_files_hash[:ELM_XML].nil?
- else
- false
+ # Returns the base directory of the measure
+ def self.unzip_measure_contents(zip_file, tmp_dir)
+ Zip::ZipFile.open(zip_file.path) do |zip_file|
+ zip_file.each do |f|
+ f_path = File.join(tmp_dir, f.name)
+ FileUtils.mkdir_p(File.dirname(f_path))
+ f.extract(f_path)
end
end
+ current_directory = tmp_dir
+ # Detect if the zip file contents were stored into a single directory
+ if Dir.glob("#{current_directory}/*").count < 3
+ # If there is a single folder containing the zip file contents, step into it (ignore __MACOSX file if it exists)
+ Dir.glob("#{current_directory}/*").select.each do |file|
+ if !file.end_with?('__MACOSX') && File.directory?(file)
+ current_directory = file
+ break
+ end
+ end
+ end
+ return current_directory
end
- def self.load_mat_cql_exports(user, zip_file, out_dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
- measure = nil
- cql = nil
- hqmf_path = nil
- # Grabs the cql file contents, the elm_xml contents, elm_json contents and the hqmf file path
- files = get_files_from_zip(zip_file, out_dir)
+ # Verifies contents of the given measure are valid (works for regular, composite and component measures)
+ def self.valid_measure_contents?(measure_dir, check_components = false)
+ # If composite measure given, check components
+ if check_components
+ Dir.glob("#{measure_dir}/*").each do |file|
+ if File.directory?(file)
+ if !valid_measure_contents?(file)
+ return false
+ end
+ end
+ end
+ end
- # Load hqmf into HQMF Parser
- hqmf_model = Measures::Loader.parse_hqmf_model(files[:HQMF_XML_PATH])
+ # Grab all cql, elm & human readable docs from measure
+ cql_entry = Dir.glob(File.join(measure_dir,'**.cql')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
+ elm_json = Dir.glob(File.join(measure_dir,'**.json')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
+ human_readable_entry = Dir.glob(File.join(measure_dir,'**.html')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
- # Get main measure from hqmf parser
- main_cql_library = hqmf_model.cql_measure_library
+ # Grab all xml files in the measure.
+ xml_files = Dir.glob(File.join(measure_dir,'**.xml')).select {|x| !File.basename(x).starts_with?('__MACOSX') }
- cql_artifacts = process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, hqmf_model.hqmf_set_id)
-
- # Create CQL Measure
- hqmf_model.backfill_patient_characteristics_with_codes(cql_artifacts[:all_codes_and_code_names])
- json = hqmf_model.to_json
- json.convert_keys_to_strings
-
- # Set the code list ids of data criteria and source data criteria that use direct reference codes to GUIDS.
- json['source_data_criteria'], json['data_criteria'] = set_data_criteria_code_list_ids(json, cql_artifacts)
-
- # Create CQL Measure
- 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],
- cql_artifacts[:elms], cql_artifacts[:elm_annotations], files[:CQL], nil, cql_artifacts[:value_set_oid_version_objects])
- measure['episode_of_care'] = measure_details['episode_of_care']
- measure['type'] = measure_details['type']
- measure['calculate_sdes'] = measure_details['calculate_sdes']
-
- # Create, associate and save the measure package.
- measure.package = CqlMeasurePackage.new(file: BSON::Binary.new(zip_file.read()))
- measure.package.save
-
- measure
+ # Find key value pair for HQMF and ELM xml files.
+ if xml_files.count > 0
+ xml_files_hash = retrieve_elm_and_hqmf(xml_files)
+ !cql_entry.nil? && !elm_json.nil? && !human_readable_entry.nil? && !xml_files_hash[:HQMF_XML].nil? && !xml_files_hash[:ELM_XML].nil?
+ else
+ false
+ end
end
def self.set_data_criteria_code_list_ids(json, cql_artifacts)
# Loop over data criteria to search for data criteria that is using a single reference code.
# Once found set the Data Criteria's 'code_list_id' to our fake oid. Do the same for source data criteria.
@@ -82,32 +113,179 @@
end
end
return json['source_data_criteria'], json['data_criteria']
end
- def self.load(file, user, measure_details, vsac_options, vsac_ticket_granting_ticket)
+ # Returns an array of measures
+ # Single measure returned into the array if it is a non-composite measure
+ def self.extract_measures(measure_zip, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
measure = nil
- Dir.mktmpdir do |dir|
- measure = load_mat_cql_exports(user, file, dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
+ component_measures = []
+ # Unzip measure contents while retaining the directory structure
+ Dir.mktmpdir do |tmp_dir|
+ current_directory = unzip_measure_contents(measure_zip, tmp_dir)
+ if !valid_measure_contents?(current_directory, true)
+ raise MeasureLoadingException.new("Zip file was not a MAT package.")
+ end
+ component_elms = {}
+ component_elms[:ELM_JSON] = []
+
+ # If it is a composite measure, load in each of the components
+ # Components must be loaded first so their elms can be passed onto the composite
+ if composite_measure?(current_directory)
+ component_measures = create_component_measures(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
+ component_measures.each do |component_measure|
+ component_elms[:ELM_JSON].push(*component_measure.elm)
+ end
+ end
+
+ # Load in regular/composite measure measure
+ begin
+ measure = create_measure(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket, component_elms)
+ rescue => e
+ component_measures.each { |component| component.delete }
+ raise e
+ end
+
+ # Create, associate and save the measure package.
+ measure_package = CqlMeasurePackage.new(file: BSON::Binary.new(measure_zip.read()))
+ measure.package = measure_package
+ measure.package.save
+
+ component_measures.each do |component_measure|
+ # Update the components' hqmf_set_id, formatted as follows:
+ # <composite_hqmf_set_id>&<component_hqmf_set_id>
+ component_measure.hqmf_set_id = measure.hqmf_set_id + '&' + component_measure.hqmf_set_id
+ component_measure.component = true;
+ # Associate the component with the composite
+ measure.component_hqmf_set_ids.push(component_measure.hqmf_set_id)
+ end
+ end # End of temporary directory usage
+
+ # Put measure (and component measures) into an array to return
+ measures = component_measures << measure
+ return measures
+ end
+
+ # Creates a composite's component measures
+ def self.create_component_measures(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
+ component_measures = []
+ Dir.glob("#{current_directory}/*").sort.each do |file|
+ if File.directory?(file)
+ component_measures << create_measure(file, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
+ end
end
+ component_measures
+ end
+
+ # Creates and returns a measure
+ def self.create_measure(measure_dir, user, measure_details, vsac_options, vsac_ticket_granting_ticket, component_elms=nil)
+ measure = nil
+
+ # Grabs the cql file contents, the elm_xml contents, elm_json contents and the hqmf file path
+ files = get_files_from_directory(measure_dir)
+
+ # Load hqmf into HQMF Parser
+ hqmf_model = Measures::Loader.parse_hqmf_model(files[:HQMF_XML_PATH])
+
+ # Get main measure from hqmf parser
+ main_cql_library = hqmf_model.cql_measure_library
+
+ cql_artifacts = process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, hqmf_model.hqmf_set_id, component_elms)
+
+ # Create CQL Measure
+ hqmf_model.backfill_patient_characteristics_with_codes(cql_artifacts[:all_codes_and_code_names])
+ json = hqmf_model.to_json
+ json.convert_keys_to_strings
+
+ # Set the code list ids of data criteria and source data criteria that use direct reference codes to GUIDS.
+ json['source_data_criteria'], json['data_criteria'] = set_data_criteria_code_list_ids(json, cql_artifacts)
+
+ # Create CQL Measure
+ measure_details["composite"] = composite_measure?(measure_dir)
+ 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],
+ cql_artifacts[:elms], cql_artifacts[:elm_annotations], files[:CQL], measure_details, cql_artifacts[:value_set_oid_version_objects])
measure
end
+ def self.get_files_from_directory(dir)
+ cql_paths = Dir.glob(File.join("#{dir}/**.cql")).sort
+ xml_paths = Dir.glob(File.join("#{dir}/**.xml")).sort
+ elm_json_paths = Dir.glob(File.join("#{dir}/**.json")).sort
+
+ begin
+ cql_contents = []
+ cql_paths.each do |cql_path|
+ cql_contents << open(cql_path).read
+ end
+
+ elm_json = []
+ elm_json_paths.each do |elm_json_path|
+ elm_json << open(elm_json_path).read
+ end
+
+ xml_file_hash = retrieve_elm_and_hqmf(xml_paths)
+ elm_xml_paths = xml_file_hash[:ELM_XML]
+ elm_xml = []
+ elm_xml_paths.each do |elm_xml_path|
+ elm_xml << open(elm_xml_path).read
+ end
+
+ files = { :HQMF_XML_PATH => xml_file_hash[:HQMF_XML],
+ :ELM_JSON => elm_json,
+ :CQL => cql_contents,
+ :ELM_XML => elm_xml }
+ return files
+ rescue Exception => e
+ raise MeasureLoadingException.new "Error Parsing Measure Logic: #{e.message}"
+ end
+ end
+
+ # Takes in array of xml files and returns hash with keys HQMF_XML and ELM_XML
+ def self.retrieve_elm_and_hqmf(files)
+ file_paths_hash = {}
+ file_paths_hash[:ELM_XML] = []
+ begin
+ files.each do |xml_file_path|
+ if xml_file_path && xml_file_path.size > 0
+ # Open up xml file and read contents.
+ doc = Nokogiri::XML.parse(File.read(xml_file_path))
+ # Check if root node in xml file matches either the HQMF file or ELM file.
+ if doc.root.name == 'QualityMeasureDocument' # Root node for HQMF XML
+ file_paths_hash[:HQMF_XML] = xml_file_path
+ elsif doc.root.name == 'library' # Root node for ELM XML
+ file_paths_hash[:ELM_XML] << xml_file_path
+ end
+ end
+ end
+ rescue Exception => e
+ raise MeasureLoadingException.new "Error Checking MAT Export: #{e.message}"
+ end
+ file_paths_hash
+ end
+
# Manages all of the CQL processing that is not related to the HQMF.
- def self.process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, measure_id=nil)
+ def self.process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, measure_id=nil, component_elms=nil)
elm_strings = files[:ELM_JSON]
# Removes 'urn:oid:' from ELM for Bonnie and Parse the JSON
elm_strings.each { |elm_string| elm_string.gsub! 'urn:oid:', '' }
elms = elm_strings.map{ |elm| JSON.parse(elm, :max_nesting=>1000)}
elm_annotations = parse_elm_annotations(files[:ELM_XML])
+ if (!component_elms.nil?)
+ elms.push(*component_elms[:ELM_JSON])
+ end
# Hash of define statements to which define statements they use.
cql_definition_dependency_structure = populate_cql_definition_dependency_structure(main_cql_library, elms)
- # Go back for the library statements
- cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
- # Add unused libraries to structure and set the value to empty hash
- cql_definition_dependency_structure = populate_unused_included_libraries(cql_definition_dependency_structure, elms)
+ begin
+ # Go back for the library statements
+ cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
+ # Add unused libraries to structure and set the value to empty hash
+ cql_definition_dependency_structure = populate_unused_included_libraries(cql_definition_dependency_structure, elms)
+ rescue => e
+ raise MeasureLoadingException.new("Measure package missing a library or component.")
+ end
# fix up statement names in cql_statement_dependencies to not use periods <<WRAP 1>>
# this is matched with an UNWRAP in MeasuresController in the bonnie project
Measures::MongoHashKeyWrapper::wrapKeys cql_definition_dependency_structure
@@ -175,11 +353,11 @@
:value_set_oid_version_objects => value_set_oid_version_objects,
:single_code_references => single_code_references,
:all_codes_and_code_names => all_codes_and_code_names}
end
- # returns a list of objects that include the valueset oids and their versions
+ # Returns a list of objects that include the valueset oids and their versions
def self.get_value_set_oid_version_objects(value_sets, single_code_references)
# [LDC] need to make this an array of objects instead of a hash because Mongo is
# dumb and *let's you* have dots in keys on object creation but *doesn't let you*
# have dots in keys on object update or retrieve....
value_set_oid_version_objects = []
@@ -264,53 +442,9 @@
end
end
end
# Returns a list of single code objects and a complete list of code systems and codes for all valuesets on the measure.
return single_code_references, all_codes_and_code_names
- end
-
- # Opens the zip and grabs the cql file contents, the ELM contents (XML and JSON) and hqmf_path.
- def self.get_files_from_zip(zip_file, out_dir)
- Zip::ZipFile.open(zip_file.path) do |file|
- cql_entries = file.glob(File.join('**','**.cql')).select {|x| !x.name.starts_with?('__MACOSX') }
- zip_xml_files = file.glob(File.join('**','**.xml')).select {|x| !x.name.starts_with?('__MACOSX') }
- elm_json_entries = file.glob(File.join('**','**.json')).select {|x| !x.name.starts_with?('__MACOSX') }
-
- begin
- cql_paths = []
- cql_entries.each do |cql_file|
- cql_paths << extract(file, cql_file, out_dir) if cql_file.size > 0
- end
- cql_contents = []
- cql_paths.each do |cql_path|
- cql_contents << open(cql_path).read
- end
-
- elm_json_paths = []
- elm_json_entries.each do |json_file|
- elm_json_paths << extract(file, json_file, out_dir) if json_file.size > 0
- end
- elm_json = []
- elm_json_paths.each do |elm_json_path|
- elm_json << open(elm_json_path).read
- end
-
- xml_file_paths = extract_xml_files(file, zip_xml_files, out_dir)
- elm_xml_paths = xml_file_paths[:ELM_XML]
- elm_xml = []
- elm_xml_paths.each do |elm_xml_path|
- elm_xml << open(elm_xml_path).read
- end
-
- files = { :HQMF_XML_PATH => xml_file_paths[:HQMF_XML],
- :ELM_JSON => elm_json,
- :CQL => cql_contents,
- :ELM_XML => elm_xml }
- return files
- rescue Exception => e
- raise MeasureLoadingException.new "Error Parsing Measure Logic: #{e.message}"
- end
- end
end
private
def self.parse_elm_annotations(xmls)
elm_annotations = {}