bonnie_bundler 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/.github/gitleaks.toml +268 -0
  3. data/.github/workflows/main.yml +31 -0
  4. data/.travis.yml +2 -2
  5. data/Gemfile.lock +74 -75
  6. data/LICENSE.txt +201 -0
  7. data/README.md +12 -0
  8. data/bonnie-bundler.gemspec +4 -4
  9. data/lib/bonnie_bundler.rb +0 -1
  10. data/lib/measures/elm_parser.rb +7 -1
  11. data/lib/measures/loading/cql_loader.rb +234 -100
  12. data/lib/measures/loading/exceptions.rb +4 -4
  13. data/lib/measures/loading/loader.rb +18 -24
  14. data/lib/models/cql_measure.rb +25 -0
  15. data/lib/util/vsac_api.rb +13 -13
  16. data/notice.md +9 -0
  17. data/test/fixtures/CMSAWA_v5_6_Artifacts.zip +0 -0
  18. data/test/fixtures/CMSAWA_v5_6_Artifacts_missing_component.zip +0 -0
  19. data/test/fixtures/CMSAWA_v5_6_Artifacts_missing_composite_files.zip +0 -0
  20. data/test/fixtures/CMSAWA_v5_6_Artifacts_missing_file.zip +0 -0
  21. data/test/fixtures/TOB2_v5_5_Artifacts.zip +0 -0
  22. data/test/fixtures/not_mat_export.zip +0 -0
  23. data/test/fixtures/vcr_cassettes/load_composite_measure.yml +7305 -0
  24. data/test/fixtures/vcr_cassettes/load_composite_measure_with_missing_component.yml +6923 -0
  25. data/test/fixtures/vcr_cassettes/load_composite_measure_with_missing_composite_files.yml +57 -0
  26. data/test/fixtures/vcr_cassettes/load_composite_measure_with_missing_file.yml +5938 -0
  27. data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +156 -167
  28. data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +216 -191
  29. data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +112 -116
  30. data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +121 -125
  31. data/test/fixtures/vcr_cassettes/valid_vsac_response_hospice.yml +523 -447
  32. data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +388 -356
  33. data/test/fixtures/vcr_cassettes/valid_vsac_response_pvc_unused_libraries.yml +969 -2079
  34. data/test/fixtures/vcr_cassettes/valid_vsac_response_special_characters.yml +5408 -0
  35. data/test/fixtures/vcr_cassettes/vs_loading_500_response.yml +26 -510
  36. data/test/fixtures/vcr_cassettes/vs_loading_empty_concept_list.yml +26 -26
  37. data/test/fixtures/vcr_cassettes/vs_loading_release.yml +106 -106
  38. data/test/fixtures/vcr_cassettes/vs_loading_version.yml +106 -106
  39. data/test/test_helper.rb +1 -1
  40. data/test/unit/composite_cql_loader_test.rb +102 -0
  41. data/test/unit/cql_loader_test.rb +38 -6
  42. data/test/unit/get_value_sets_from_measure_model_test.rb +34 -0
  43. data/test/unit/load_mat_export_test.rb +15 -9
  44. data/test/unit/measure_complexity_test.rb +1 -1
  45. data/test/unit/measure_diff_test.rb +4 -4
  46. data/test/unit/storing_mat_export_package_test.rb +1 -2
  47. data/test/unit/value_set_loading_test.rb +16 -16
  48. data/test/unit/vsac_api_auth_test.rb +7 -31
  49. data/test/unit/vsac_api_test.rb +6 -6
  50. data/test/vcr_setup.rb +4 -6
  51. metadata +35 -14
  52. data/lib/measures/loading/base_loader_definition.rb +0 -61
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md CHANGED
@@ -2,3 +2,15 @@ bonnie_bundler
2
2
  ==============
3
3
 
4
4
  Gem for creating and managing bonnie bundles
5
+
6
+ ## License
7
+
8
+ Copyright 2014 The MITRE Corporation
9
+
10
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
11
+
12
+ ```
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+ ```
15
+
16
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
@@ -7,23 +7,23 @@ Gem::Specification.new do |s|
7
7
  s.email = "pophealth-talk@googlegroups.com"
8
8
  s.homepage = "http://github.com/projecttacoma/bonnie_bundler"
9
9
  s.authors = ["The MITRE Corporation"]
10
- s.version = '2.2.0'
10
+ s.version = '3.0.0'
11
11
  s.license = 'Apache-2.0'
12
12
 
13
- s.add_dependency 'health-data-standards', '~> 4.0'
13
+ s.add_dependency 'health-data-standards', '~> 4.3.2'
14
14
  s.add_dependency 'quality-measure-engine', '~> 3.2'
15
15
  s.add_dependency 'hquery-patient-api', '~> 1.1'
16
16
  s.add_dependency 'simplexml_parser', '~> 1.0'
17
17
  s.add_dependency 'hqmf2js', '~> 1.4'
18
18
 
19
- s.add_dependency 'rails', '~> 4.2'
19
+ s.add_dependency 'rails', '>= 4.2', '< 6.0'
20
20
  s.add_dependency 'mongoid', '~> 5.0'
21
21
  s.add_dependency 'rubyzip', '~> 1.2', '>= 1.2.1'
22
22
  s.add_dependency 'zip-zip', '~> 0.3'
23
23
  s.add_dependency 'diffy', '~> 3.0.0'
24
24
 
25
25
  # needed for parsing value sets (we need to use roo rather than rubyxl because the value sets are in xls rather than xlsx)
26
- s.add_dependency 'roo', '~> 1.13'
26
+ s.add_dependency 'roo', '~> 2.7'
27
27
 
28
28
  s.files = s.files = `git ls-files`.split("\n")
29
29
  end
@@ -14,7 +14,6 @@ require_relative 'models/measure.rb'
14
14
  require_relative 'models/cql_measure.rb'
15
15
  require_relative 'measures/loading/exceptions.rb'
16
16
  require_relative 'measures/loading/loader.rb'
17
- require_relative 'measures/loading/base_loader_definition.rb'
18
17
  require_relative 'measures/loading/cql_loader.rb'
19
18
  require_relative 'measures/loading/value_set_loader.rb'
20
19
  require_relative 'measures/logic_extractor.rb'
@@ -2,6 +2,7 @@ module CqlElm
2
2
  class Parser
3
3
  #Fields are combined with the refId to find elm node that corrosponds to the current annotation node.
4
4
  @fields = ['expression', 'operand', 'suchThat']
5
+ @html_hash = {"&amp;": '&', "&quot;": '"', "&lt;": '<', "&gt;": '>', "&apos;": "'"}
5
6
  @previousNoTrailingSpaceNotPeriod = false
6
7
 
7
8
  def self.parse(elm_xml)
@@ -47,9 +48,14 @@ module CqlElm
47
48
  else
48
49
  if (/^define/ =~ child.to_html)
49
50
  define_name = child.to_html.split("\"")[1]
51
+ # Modify special characters back in the the define_name
52
+ @html_hash.each { |k,v| define_name.gsub!(k.to_s, v) }
50
53
  end
54
+ clause_text = child.to_html.gsub(/\t/, " ")
55
+ # Modify special characters back in the clause text
56
+ @html_hash.each { |k,v| clause_text.gsub!(k.to_s, v) }
51
57
  clause = {
52
- text: child.to_html.gsub(/\t/, " ")
58
+ text: clause_text
53
59
  }
54
60
  clause[:ref_id] = child['r'] unless child['r'].nil?
55
61
  ret[:children] << clause
@@ -1,62 +1,93 @@
1
1
  module Measures
2
2
  # Utility class for loading CQL measure definitions into the database from the MAT export zip
3
- class CqlLoader < BaseLoaderDefinition
3
+ class CqlLoader
4
+ # Returns true if ths uploaded measure zip file is a composite measure
5
+ def self.composite_measure?(measure_dir)
6
+ # Look through all xml files at current directory level and find QDM
7
+ files = Dir.glob("#{measure_dir}/**.xml").select
8
+ begin
9
+ # Iterate over all files passed in, extract file to temporary directory.
10
+ files.each do |xml_file|
11
+ if xml_file && xml_file.size > 0
12
+ # Open up xml file and read contents.
13
+ doc = Nokogiri::XML.parse(File.read(xml_file))
14
+ # Check if root node in xml file matches either the HQMF file or ELM file.
15
+ if doc.root.name == 'QualityMeasureDocument' # Root node for HQMF XML
16
+ # Xpath to determine if it is a composite or not
17
+ doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
18
+ return !doc.at_xpath('//cda:measureAttribute[cda:code[@code="MSRTYPE"]][cda:value[@code="COMPOSITE"]]').nil?
19
+ end
20
+ end
21
+ end
22
+ rescue Exception => e
23
+ raise MeasureLoadingException.new "Error Checking MAT Export: #{e.message}"
24
+ end
25
+ false
26
+ end
4
27
 
28
+ # Verifies that the zip file contains a valid measure
29
+ # Works for both regular & composite measures
5
30
  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') }
31
+ # Extract contents of zip file while retaining the directory structure
32
+ original = Dir.pwd
33
+ Dir.mktmpdir do |tmp_dir|
34
+ current_directory = unzip_measure_contents(zip_file, tmp_dir)
35
+ # Check if measure contents are valid
36
+ return valid_measure_contents?(current_directory, true)
37
+ end
38
+ end
15
39
 
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
40
+ # Returns the base directory of the measure
41
+ def self.unzip_measure_contents(zip_file, tmp_dir)
42
+ Zip::ZipFile.open(zip_file.path) do |zip_file|
43
+ zip_file.each do |f|
44
+ f_path = File.join(tmp_dir, f.name)
45
+ FileUtils.mkdir_p(File.dirname(f_path))
46
+ f.extract(f_path)
47
+ end
48
+ end
49
+ current_directory = tmp_dir
50
+ # Detect if the zip file contents were stored into a single directory
51
+ if Dir.glob("#{current_directory}/*").count < 3
52
+ # If there is a single folder containing the zip file contents, step into it (ignore __MACOSX file if it exists)
53
+ Dir.glob("#{current_directory}/*").select.each do |file|
54
+ if !file.end_with?('__MACOSX') && File.directory?(file)
55
+ current_directory = file
56
+ break
57
+ end
21
58
  end
22
59
  end
60
+ return current_directory
23
61
  end
24
62
 
25
- def self.load_mat_cql_exports(user, zip_file, out_dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
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_options, vsac_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
- # Set the code list ids of data criteria and source data criteria that use direct reference codes to GUIDS.
46
- json['source_data_criteria'], json['data_criteria'] = set_data_criteria_code_list_ids(json, cql_artifacts)
63
+ # Verifies contents of the given measure are valid (works for regular, composite and component measures)
64
+ def self.valid_measure_contents?(measure_dir, check_components = false)
65
+ # If composite measure given, check components
66
+ if check_components
67
+ Dir.glob("#{measure_dir}/*").each do |file|
68
+ if File.directory?(file)
69
+ if !valid_measure_contents?(file)
70
+ return false
71
+ end
72
+ end
73
+ end
74
+ end
47
75
 
48
- # Create CQL Measure
49
- 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],
50
- cql_artifacts[:elms], cql_artifacts[:elm_annotations], files[:CQL], nil, cql_artifacts[:value_set_oid_version_objects])
51
- measure['episode_of_care'] = measure_details['episode_of_care']
52
- measure['type'] = measure_details['type']
53
- measure['calculate_sdes'] = measure_details['calculate_sdes']
76
+ # Grab all cql, elm & human readable docs from measure
77
+ cql_entry = Dir.glob(File.join(measure_dir,'**.cql')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
78
+ elm_json = Dir.glob(File.join(measure_dir,'**.json')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
79
+ human_readable_entry = Dir.glob(File.join(measure_dir,'**.html')).select {|x| !File.basename(x).starts_with?('__MACOSX') }.first
54
80
 
55
- # Create, associate and save the measure package.
56
- measure.package = CqlMeasurePackage.new(file: BSON::Binary.new(zip_file.read()))
57
- measure.package.save
81
+ # Grab all xml files in the measure.
82
+ xml_files = Dir.glob(File.join(measure_dir,'**.xml')).select {|x| !File.basename(x).starts_with?('__MACOSX') }
58
83
 
59
- measure
84
+ # Find key value pair for HQMF and ELM xml files.
85
+ if xml_files.count > 0
86
+ xml_files_hash = retrieve_elm_and_hqmf(xml_files)
87
+ !cql_entry.nil? && !elm_json.nil? && !human_readable_entry.nil? && !xml_files_hash[:HQMF_XML].nil? && !xml_files_hash[:ELM_XML].nil?
88
+ else
89
+ false
90
+ end
60
91
  end
61
92
 
62
93
  def self.set_data_criteria_code_list_ids(json, cql_artifacts)
@@ -84,28 +115,175 @@ module Measures
84
115
  return json['source_data_criteria'], json['data_criteria']
85
116
  end
86
117
 
87
- def self.load(file, user, measure_details, vsac_options, vsac_ticket_granting_ticket)
118
+ # Returns an array of measures
119
+ # Single measure returned into the array if it is a non-composite measure
120
+ def self.extract_measures(measure_zip, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
88
121
  measure = nil
89
- Dir.mktmpdir do |dir|
90
- measure = load_mat_cql_exports(user, file, dir, measure_details, vsac_options, vsac_ticket_granting_ticket)
122
+ component_measures = []
123
+ # Unzip measure contents while retaining the directory structure
124
+ Dir.mktmpdir do |tmp_dir|
125
+ current_directory = unzip_measure_contents(measure_zip, tmp_dir)
126
+ if !valid_measure_contents?(current_directory, true)
127
+ raise MeasureLoadingException.new("Zip file was not a MAT package.")
128
+ end
129
+ component_elms = {}
130
+ component_elms[:ELM_JSON] = []
131
+
132
+ # If it is a composite measure, load in each of the components
133
+ # Components must be loaded first so their elms can be passed onto the composite
134
+ if composite_measure?(current_directory)
135
+ component_measures = create_component_measures(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
136
+ component_measures.each do |component_measure|
137
+ component_elms[:ELM_JSON].push(*component_measure.elm)
138
+ end
139
+ end
140
+
141
+ # Load in regular/composite measure measure
142
+ begin
143
+ measure = create_measure(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket, component_elms)
144
+ rescue => e
145
+ component_measures.each { |component| component.delete }
146
+ raise e
147
+ end
148
+
149
+ # Create, associate and save the measure package.
150
+ measure_package = CqlMeasurePackage.new(file: BSON::Binary.new(measure_zip.read()))
151
+ measure.package = measure_package
152
+ measure.package.save
153
+
154
+ component_measures.each do |component_measure|
155
+ # Update the components' hqmf_set_id, formatted as follows:
156
+ # <composite_hqmf_set_id>&<component_hqmf_set_id>
157
+ component_measure.hqmf_set_id = measure.hqmf_set_id + '&' + component_measure.hqmf_set_id
158
+ component_measure.component = true;
159
+ # Associate the component with the composite
160
+ measure.component_hqmf_set_ids.push(component_measure.hqmf_set_id)
161
+ end
162
+ end # End of temporary directory usage
163
+
164
+ # Put measure (and component measures) into an array to return
165
+ measures = component_measures << measure
166
+ return measures
167
+ end
168
+
169
+ # Creates a composite's component measures
170
+ def self.create_component_measures(current_directory, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
171
+ component_measures = []
172
+ Dir.glob("#{current_directory}/*").sort.each do |file|
173
+ if File.directory?(file)
174
+ component_measures << create_measure(file, current_user, measure_details, vsac_options, vsac_ticket_granting_ticket)
175
+ end
91
176
  end
177
+ component_measures
178
+ end
179
+
180
+ # Creates and returns a measure
181
+ def self.create_measure(measure_dir, user, measure_details, vsac_options, vsac_ticket_granting_ticket, component_elms=nil)
182
+ measure = nil
183
+
184
+ # Grabs the cql file contents, the elm_xml contents, elm_json contents and the hqmf file path
185
+ files = get_files_from_directory(measure_dir)
186
+
187
+ # Load hqmf into HQMF Parser
188
+ hqmf_model = Measures::Loader.parse_hqmf_model(files[:HQMF_XML_PATH])
189
+
190
+ # Get main measure from hqmf parser
191
+ main_cql_library = hqmf_model.cql_measure_library
192
+
193
+ cql_artifacts = process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, hqmf_model.hqmf_set_id, component_elms)
194
+
195
+ # Create CQL Measure
196
+ hqmf_model.backfill_patient_characteristics_with_codes(cql_artifacts[:all_codes_and_code_names])
197
+ json = hqmf_model.to_json
198
+ json.convert_keys_to_strings
199
+
200
+ # Set the code list ids of data criteria and source data criteria that use direct reference codes to GUIDS.
201
+ json['source_data_criteria'], json['data_criteria'] = set_data_criteria_code_list_ids(json, cql_artifacts)
202
+
203
+ # Create CQL Measure
204
+ measure_details["composite"] = composite_measure?(measure_dir)
205
+ 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],
206
+ cql_artifacts[:elms], cql_artifacts[:elm_annotations], files[:CQL], measure_details, cql_artifacts[:value_set_oid_version_objects])
92
207
  measure
93
208
  end
94
209
 
210
+ def self.get_files_from_directory(dir)
211
+ cql_paths = Dir.glob(File.join("#{dir}/**.cql")).sort
212
+ xml_paths = Dir.glob(File.join("#{dir}/**.xml")).sort
213
+ elm_json_paths = Dir.glob(File.join("#{dir}/**.json")).sort
214
+
215
+ begin
216
+ cql_contents = []
217
+ cql_paths.each do |cql_path|
218
+ cql_contents << open(cql_path).read
219
+ end
220
+
221
+ elm_json = []
222
+ elm_json_paths.each do |elm_json_path|
223
+ elm_json << open(elm_json_path).read
224
+ end
225
+
226
+ xml_file_hash = retrieve_elm_and_hqmf(xml_paths)
227
+ elm_xml_paths = xml_file_hash[:ELM_XML]
228
+ elm_xml = []
229
+ elm_xml_paths.each do |elm_xml_path|
230
+ elm_xml << open(elm_xml_path).read
231
+ end
232
+
233
+ files = { :HQMF_XML_PATH => xml_file_hash[:HQMF_XML],
234
+ :ELM_JSON => elm_json,
235
+ :CQL => cql_contents,
236
+ :ELM_XML => elm_xml }
237
+ return files
238
+ rescue Exception => e
239
+ raise MeasureLoadingException.new "Error Parsing Measure Logic: #{e.message}"
240
+ end
241
+ end
242
+
243
+ # Takes in array of xml files and returns hash with keys HQMF_XML and ELM_XML
244
+ def self.retrieve_elm_and_hqmf(files)
245
+ file_paths_hash = {}
246
+ file_paths_hash[:ELM_XML] = []
247
+ begin
248
+ files.each do |xml_file_path|
249
+ if xml_file_path && xml_file_path.size > 0
250
+ # Open up xml file and read contents.
251
+ doc = Nokogiri::XML.parse(File.read(xml_file_path))
252
+ # Check if root node in xml file matches either the HQMF file or ELM file.
253
+ if doc.root.name == 'QualityMeasureDocument' # Root node for HQMF XML
254
+ file_paths_hash[:HQMF_XML] = xml_file_path
255
+ elsif doc.root.name == 'library' # Root node for ELM XML
256
+ file_paths_hash[:ELM_XML] << xml_file_path
257
+ end
258
+ end
259
+ end
260
+ rescue Exception => e
261
+ raise MeasureLoadingException.new "Error Checking MAT Export: #{e.message}"
262
+ end
263
+ file_paths_hash
264
+ end
265
+
95
266
  # Manages all of the CQL processing that is not related to the HQMF.
96
- def self.process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, measure_id=nil)
267
+ def self.process_cql(files, main_cql_library, user, vsac_options, vsac_ticket_granting_ticket, measure_id=nil, component_elms=nil)
97
268
  elm_strings = files[:ELM_JSON]
98
269
  # Removes 'urn:oid:' from ELM for Bonnie and Parse the JSON
99
270
  elm_strings.each { |elm_string| elm_string.gsub! 'urn:oid:', '' }
100
271
  elms = elm_strings.map{ |elm| JSON.parse(elm, :max_nesting=>1000)}
101
272
  elm_annotations = parse_elm_annotations(files[:ELM_XML])
102
273
 
274
+ if (!component_elms.nil?)
275
+ elms.push(*component_elms[:ELM_JSON])
276
+ end
103
277
  # Hash of define statements to which define statements they use.
104
278
  cql_definition_dependency_structure = populate_cql_definition_dependency_structure(main_cql_library, elms)
105
- # Go back for the library statements
106
- cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
107
- # Add unused libraries to structure and set the value to empty hash
108
- cql_definition_dependency_structure = populate_unused_included_libraries(cql_definition_dependency_structure, elms)
279
+ begin
280
+ # Go back for the library statements
281
+ cql_definition_dependency_structure = populate_used_library_dependencies(cql_definition_dependency_structure, main_cql_library, elms)
282
+ # Add unused libraries to structure and set the value to empty hash
283
+ cql_definition_dependency_structure = populate_unused_included_libraries(cql_definition_dependency_structure, elms)
284
+ rescue => e
285
+ raise MeasureLoadingException.new("Measure package missing a library or component.")
286
+ end
109
287
 
110
288
  # fix up statement names in cql_statement_dependencies to not use periods <<WRAP 1>>
111
289
  # this is matched with an UNWRAP in MeasuresController in the bonnie project
@@ -177,7 +355,7 @@ module Measures
177
355
  :all_codes_and_code_names => all_codes_and_code_names}
178
356
  end
179
357
 
180
- # returns a list of objects that include the valueset oids and their versions
358
+ # Returns a list of objects that include the valueset oids and their versions
181
359
  def self.get_value_set_oid_version_objects(value_sets, single_code_references)
182
360
  # [LDC] need to make this an array of objects instead of a hash because Mongo is
183
361
  # dumb and *let's you* have dots in keys on object creation but *doesn't let you*
@@ -268,50 +446,6 @@ module Measures
268
446
  return single_code_references, all_codes_and_code_names
269
447
  end
270
448
 
271
- # Opens the zip and grabs the cql file contents, the ELM contents (XML and JSON) and hqmf_path.
272
- def self.get_files_from_zip(zip_file, out_dir)
273
- Zip::ZipFile.open(zip_file.path) do |file|
274
- cql_entries = file.glob(File.join('**','**.cql')).select {|x| !x.name.starts_with?('__MACOSX') }
275
- zip_xml_files = file.glob(File.join('**','**.xml')).select {|x| !x.name.starts_with?('__MACOSX') }
276
- elm_json_entries = file.glob(File.join('**','**.json')).select {|x| !x.name.starts_with?('__MACOSX') }
277
-
278
- begin
279
- cql_paths = []
280
- cql_entries.each do |cql_file|
281
- cql_paths << extract(file, cql_file, out_dir) if cql_file.size > 0
282
- end
283
- cql_contents = []
284
- cql_paths.each do |cql_path|
285
- cql_contents << open(cql_path).read
286
- end
287
-
288
- elm_json_paths = []
289
- elm_json_entries.each do |json_file|
290
- elm_json_paths << extract(file, json_file, out_dir) if json_file.size > 0
291
- end
292
- elm_json = []
293
- elm_json_paths.each do |elm_json_path|
294
- elm_json << open(elm_json_path).read
295
- end
296
-
297
- xml_file_paths = extract_xml_files(file, zip_xml_files, out_dir)
298
- elm_xml_paths = xml_file_paths[:ELM_XML]
299
- elm_xml = []
300
- elm_xml_paths.each do |elm_xml_path|
301
- elm_xml << open(elm_xml_path).read
302
- end
303
-
304
- files = { :HQMF_XML_PATH => xml_file_paths[:HQMF_XML],
305
- :ELM_JSON => elm_json,
306
- :CQL => cql_contents,
307
- :ELM_XML => elm_xml }
308
- return files
309
- rescue Exception => e
310
- raise MeasureLoadingException.new "Error Parsing Measure Logic: #{e.message}"
311
- end
312
- end
313
- end
314
-
315
449
  private
316
450
  def self.parse_elm_annotations(xmls)
317
451
  elm_annotations = {}