health-data-standards 3.5.3 → 3.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -3
  3. data/README.md +10 -2
  4. data/lib/health-data-standards.rb +5 -28
  5. data/lib/health-data-standards/export/cat_1.rb +3 -2
  6. data/lib/health-data-standards/export/cat_1_r2.rb +11 -0
  7. data/lib/health-data-standards/ext/node.rb +1 -2
  8. data/lib/health-data-standards/import/bulk_record_importer.rb +4 -4
  9. data/lib/health-data-standards/import/bundle/importer.rb +62 -0
  10. data/lib/health-data-standards/import/c32/insurance_provider_importer.rb +10 -8
  11. data/lib/health-data-standards/import/cat1/patient_importer.rb +3 -3
  12. data/lib/health-data-standards/import/cat1/procedure_importer.rb +42 -0
  13. data/lib/health-data-standards/import/cda/medication_importer.rb +11 -11
  14. data/lib/health-data-standards/import/cda/section_importer.rb +13 -7
  15. data/lib/health-data-standards/models/cqm/aggregate_objects.rb +5 -1
  16. data/lib/health-data-standards/models/cqm/bundle.rb +5 -3
  17. data/lib/health-data-standards/models/cqm/measure.rb +1 -1
  18. data/lib/health-data-standards/models/entry.rb +5 -1
  19. data/lib/health-data-standards/models/record.rb +10 -0
  20. data/lib/health-data-standards/models/reference.rb +23 -0
  21. data/lib/health-data-standards/models/svs/value_set.rb +4 -3
  22. data/lib/health-data-standards/railtie.rb +1 -1
  23. data/lib/health-data-standards/tasks.rb +1 -0
  24. data/lib/health-data-standards/tasks/bundle.rake +84 -2
  25. data/lib/health-data-standards/util/vs_api.rb +40 -7
  26. data/lib/health-data-standards/validate/base_validator.rb +23 -0
  27. data/lib/health-data-standards/validate/data_validator.rb +85 -0
  28. data/lib/health-data-standards/validate/measure_validator.rb +127 -0
  29. data/lib/health-data-standards/validate/performance_rate_validator.rb +94 -0
  30. data/lib/health-data-standards/validate/reported_result_extractor.rb +170 -0
  31. data/lib/health-data-standards/validate/schema_validator.rb +24 -0
  32. data/lib/health-data-standards/validate/schematron/c_processor.rb +28 -0
  33. data/lib/health-data-standards/validate/schematron/java_processor.rb +93 -0
  34. data/lib/health-data-standards/validate/schematron_validator.rb +34 -0
  35. data/lib/health-data-standards/validate/validation_error.rb +10 -0
  36. data/lib/health-data-standards/validate/validators.rb +80 -0
  37. data/lib/hqmf-generator/fulfills.xml.erb +7 -0
  38. data/lib/hqmf-generator/hqmf-generator.rb +8 -3
  39. data/lib/hqmf-model/data_criteria.rb +3 -0
  40. data/lib/hqmf-model/types.rb +29 -0
  41. data/lib/hqmf-parser/2.0/data_criteria.rb +7 -0
  42. data/lib/hqmf-parser/2.0/types.rb +24 -0
  43. data/resources/schema/infrastructure/cda/CDA_SDTC.xsd +44 -0
  44. data/resources/schema/infrastructure/cda/POCD_MT000040_SDTC.xsd +1500 -0
  45. data/resources/schema/infrastructure/cda/SDTC.xsd +210 -0
  46. data/resources/schema/processable/coreschemas/NarrativeBlock.xsd +557 -0
  47. data/resources/schema/processable/coreschemas/datatypes-base_SDTC.xsd +1850 -0
  48. data/resources/schema/processable/coreschemas/datatypes.xsd +1375 -0
  49. data/resources/schema/processable/coreschemas/infrastructureRoot.xsd +27 -0
  50. data/resources/schema/processable/coreschemas/voc.xsd +2124 -0
  51. data/resources/schematron/iso-schematron-xslt1/ExtractSchFromRNG.xsl +75 -0
  52. data/resources/schematron/iso-schematron-xslt1/ExtractSchFromXSD.xsl +77 -0
  53. data/resources/schematron/iso-schematron-xslt1/iso_abstract_expand.xsl +297 -0
  54. data/resources/schematron/iso-schematron-xslt1/iso_dsdl_include.xsl +1509 -0
  55. data/resources/schematron/iso-schematron-xslt1/iso_schematron_message.xsl +55 -0
  56. data/resources/schematron/iso-schematron-xslt1/iso_schematron_skeleton_for_xslt1.xsl +1844 -0
  57. data/resources/schematron/iso-schematron-xslt1/iso_svrl_for_xslt1.xsl +605 -0
  58. data/resources/schematron/iso-schematron-xslt1/readme.txt +101 -0
  59. data/resources/schematron/iso-schematron-xslt1/schematron-skeleton-api.htm +723 -0
  60. data/resources/schematron/iso-schematron-xslt2/ExtractSchFromRNG-2.xsl +75 -0
  61. data/resources/schematron/iso-schematron-xslt2/ExtractSchFromXSD-2.xsl +77 -0
  62. data/resources/schematron/iso-schematron-xslt2/iso_abstract_expand.xsl +297 -0
  63. data/resources/schematron/iso-schematron-xslt2/iso_dsdl_include.xsl +1508 -0
  64. data/resources/schematron/iso-schematron-xslt2/iso_schematron_message_xslt2.xsl +55 -0
  65. data/resources/schematron/iso-schematron-xslt2/iso_schematron_skeleton_for_saxon.xsl +2299 -0
  66. data/resources/schematron/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl +684 -0
  67. data/resources/schematron/iso-schematron-xslt2/readme.txt +100 -0
  68. data/resources/schematron/iso-schematron-xslt2/sch-messages-cs.xhtml +56 -0
  69. data/resources/schematron/iso-schematron-xslt2/sch-messages-de.xhtml +57 -0
  70. data/resources/schematron/iso-schematron-xslt2/sch-messages-en.xhtml +57 -0
  71. data/resources/schematron/iso-schematron-xslt2/sch-messages-fr.xhtml +54 -0
  72. data/resources/schematron/iso-schematron-xslt2/sch-messages-nl.xhtml +58 -0
  73. data/resources/schematron/iso-schematron-xslt2/schematron-skeleton-api.htm +723 -0
  74. data/resources/schematron/qrda/cat_1/CDAR2_QRDA_I_R1_D3_2015MAY_Schematron.sch +4676 -0
  75. data/resources/schematron/qrda/cat_1/voc.xml +1177 -0
  76. data/resources/schematron/qrda/cat_1_r2/QRDA Category I Release 2.sch +4069 -0
  77. data/resources/schematron/qrda/cat_1_r2/voc.xml +1065 -0
  78. data/resources/schematron/qrda/cat_3/QRDA Category III.sch +675 -0
  79. data/resources/schematron/qrda/cat_3/voc.xml +21 -0
  80. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.26.cat1.erb +18 -0
  81. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.32.cat1.erb +4 -0
  82. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.38.cat1.erb +5 -1
  83. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.4.cat1.erb +1 -0
  84. data/templates/cat1/_2.16.840.1.113883.10.20.24.3.64.cat1.erb +20 -0
  85. data/templates/cat1/_fulfills.cat1.erb +14 -0
  86. data/templates/cat1/_organization.cat1.erb +2 -1
  87. data/templates/cat1/_patient_data.cat1.erb +1 -1
  88. data/templates/cat1/show.cat1.erb +5 -4
  89. data/templates/cat3/_performance_rate.cat3.erb +5 -1
  90. data/templates/cat3/show.cat3.erb +1 -1
  91. metadata +128 -109
  92. data/lib/health-data-standards/export/ccr.rb +0 -417
  93. data/lib/health-data-standards/export/green_c32/entry.rb +0 -18
  94. data/lib/health-data-standards/export/green_c32/export_generator.rb +0 -23
  95. data/lib/health-data-standards/export/green_c32/record.rb +0 -18
  96. data/lib/health-data-standards/export/helper/gc32_view_helper.rb +0 -39
  97. data/lib/health-data-standards/import/ccr/patient_importer.rb +0 -238
  98. data/lib/health-data-standards/import/ccr/product_importer.rb +0 -60
  99. data/lib/health-data-standards/import/ccr/provider_importer.rb +0 -49
  100. data/lib/health-data-standards/import/ccr/result_importer.rb +0 -49
  101. data/lib/health-data-standards/import/ccr/section_importer.rb +0 -135
  102. data/lib/health-data-standards/import/ccr/simple_importer.rb +0 -30
  103. data/lib/health-data-standards/import/green_c32/advance_directive_importer.rb +0 -14
  104. data/lib/health-data-standards/import/green_c32/allergy_importer.rb +0 -20
  105. data/lib/health-data-standards/import/green_c32/care_goal_importer.rb +0 -26
  106. data/lib/health-data-standards/import/green_c32/condition_importer.rb +0 -38
  107. data/lib/health-data-standards/import/green_c32/encounter_importer.rb +0 -33
  108. data/lib/health-data-standards/import/green_c32/immunization_importer.rb +0 -23
  109. data/lib/health-data-standards/import/green_c32/medical_equipment_importer.rb +0 -24
  110. data/lib/health-data-standards/import/green_c32/medication_importer.rb +0 -68
  111. data/lib/health-data-standards/import/green_c32/patient_importer.rb +0 -14
  112. data/lib/health-data-standards/import/green_c32/procedure_importer.rb +0 -27
  113. data/lib/health-data-standards/import/green_c32/result_importer.rb +0 -43
  114. data/lib/health-data-standards/import/green_c32/section_importer.rb +0 -186
  115. data/lib/health-data-standards/import/green_c32/social_history_importer.rb +0 -13
  116. data/lib/health-data-standards/import/green_c32/support_importer.rb +0 -22
  117. data/lib/health-data-standards/import/green_c32/vital_sign_importer.rb +0 -21
  118. data/templates/gc32/_address.gc32.erb +0 -9
  119. data/templates/gc32/_advance_directive.gc32.erb +0 -5
  120. data/templates/gc32/_allergy.gc32.erb +0 -12
  121. data/templates/gc32/_care_goal.gc32.erb +0 -8
  122. data/templates/gc32/_condition.gc32.erb +0 -10
  123. data/templates/gc32/_encounter.gc32.erb +0 -28
  124. data/templates/gc32/_entry.gc32.erb +0 -3
  125. data/templates/gc32/_entry_attributes.gc32.erb +0 -10
  126. data/templates/gc32/_immunization.gc32.erb +0 -9
  127. data/templates/gc32/_insurance_provider.gc32.erb +0 -28
  128. data/templates/gc32/_medical_equipment.gc32.erb +0 -6
  129. data/templates/gc32/_medication.gc32.erb +0 -91
  130. data/templates/gc32/_name.gc32.erb +0 -11
  131. data/templates/gc32/_organization.gc32.erb +0 -10
  132. data/templates/gc32/_person_attributes.gc32.erb +0 -7
  133. data/templates/gc32/_procedure.gc32.erb +0 -9
  134. data/templates/gc32/_provider.gc32.erb +0 -9
  135. data/templates/gc32/_result.gc32.erb +0 -12
  136. data/templates/gc32/_social_history.gc32.erb +0 -6
  137. data/templates/gc32/_support.gc32.erb +0 -15
  138. data/templates/gc32/_telecom.gc32.erb +0 -1
  139. data/templates/gc32/_vital_sign.gc32.erb +0 -4
  140. data/templates/gc32/record.gc32.erb +0 -97
@@ -0,0 +1,127 @@
1
+ module HealthDataStandards
2
+ module Validate
3
+ class MeasureValidator
4
+ include BaseValidator
5
+
6
+ def initialize(template_oid)
7
+ @template_oid = template_oid
8
+
9
+ end
10
+
11
+ def validate(file, data={})
12
+ @errors = []
13
+ @doc = get_document(file)
14
+ @doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
15
+ measure_ids = HealthDataStandards::CQM::Measure.all.map(&:hqmf_id)
16
+ doc_measure_ids = @doc.xpath(measure_selector).map(&:value).map(&:upcase)
17
+ #list of all of the set ids in the QRDA
18
+ doc_neutral_ids = @doc.xpath(neutral_measure_selector).map(&:value).map(&:upcase).sort
19
+ #list of all of the setids in the QRDA that are also in the bundle, includes duplicates if code appears twice in document
20
+ bundle_neutral_ids = HealthDataStandards::CQM::Measure.distinct(:hqmf_set_id)
21
+ doc_bundle_neutral_ids = doc_neutral_ids - (doc_neutral_ids - bundle_neutral_ids)
22
+ validate_measure_ids(doc_measure_ids, measure_ids, data)
23
+ validate_set_ids(doc_neutral_ids, doc_bundle_neutral_ids, data)
24
+ if validate_no_repeating_measure_population_ids(data)
25
+ validate_measure_ids_set_ids_usage(doc_bundle_neutral_ids, doc_measure_ids, data)
26
+ end
27
+
28
+ @errors
29
+ end
30
+
31
+ private
32
+
33
+ #returns true if there are no repeating measures, check to see that the measure id usage is correct
34
+ def validate_no_repeating_measure_population_ids(data={})
35
+ noDuplicateMeasures = true
36
+ doc_population_ids = @doc.xpath(measure_population_selector).map(&:value).map(&:upcase).sort
37
+ duplicates = doc_population_ids.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)
38
+ duplicates.each do |duplicate|
39
+ measureId = @doc.xpath(find_measure_node_for_population(duplicate)).at_xpath("cda:reference/cda:externalDocument/cda:id[./@root='2.16.840.1.113883.4.738']/@extension")
40
+ @errors << build_error("Population #{duplicate} for Measure #{measureId.value} reported more than once", "/", data[:file_name])
41
+ noDuplicateMeasures = false
42
+ end
43
+ return noDuplicateMeasures
44
+ end
45
+
46
+ def validate_measure_ids(doc_measure_ids, measure_ids, data={})
47
+ (doc_measure_ids - measure_ids).map do |hqmf_id|
48
+ @errors << build_error("Invalid HQMF ID Found: #{hqmf_id}", "/", data[:file_name])
49
+ end
50
+ end
51
+
52
+ def validate_set_ids(doc_neutral_ids, doc_bundle_neutral_ids, data={})
53
+ #an error will be returned for all of the setids that are in the QRDA that aren't in the bundle
54
+ (doc_neutral_ids - doc_bundle_neutral_ids).map do |hqmf_set_id|
55
+ @errors << build_error("Invalid HQMF Set ID Found: #{hqmf_set_id}", "/", data[:file_name])
56
+ end
57
+ end
58
+
59
+ #does not work if the same measure is reported more than once, nested under the repeating measures test
60
+ def validate_measure_ids_set_ids_usage(doc_bundle_neutral_ids, doc_measure_ids, data={})
61
+ #for each of the setIds that are in the bundle, check that they are for the correct measure id
62
+ entries_start_position = @doc.xpath(first_entry)
63
+ previous = ""
64
+ index = 1
65
+ doc_bundle_neutral_ids.each do |hqmf_set_id|
66
+ #selects the measure id that is in the same entry as the set id
67
+ #iterates through multiple instances of the same setId
68
+ if previous == hqmf_set_id
69
+ index = index + 1
70
+ else
71
+ index = 1
72
+ end
73
+ measure_id_entry = doc_measure_ids[(@doc.xpath(location_of_set_id(hqmf_set_id,index)) - entries_start_position)]
74
+ previous = hqmf_set_id
75
+ #queries database to see if there is a measure with the combindation of setId and measureId
76
+ if HealthDataStandards::CQM::Measure.where(hqmf_id: measure_id_entry, hqmf_set_id: hqmf_set_id).length() == 0
77
+ @errors << build_error("Invalid HQMF Set ID Found: #{hqmf_set_id} for HQMF ID: #{measure_id_entry}", "/", data[:file_name])
78
+ end
79
+ end
80
+ end
81
+
82
+ def measure_selector
83
+ "/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
84
+ "/cda:organizer[./cda:templateId[@root='#{@template_oid}']]/cda:reference[@typeCode='REFR']" +
85
+ "/cda:externalDocument[@classCode='DOC']/cda:id[@root='2.16.840.1.113883.4.738']/@extension"
86
+ end
87
+
88
+ #finds all of the setIds in the QRDA document
89
+ def neutral_measure_selector
90
+ "/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
91
+ "/cda:organizer[./cda:templateId[@root='#{@template_oid}']]/cda:reference[@typeCode='REFR']" +
92
+ "/cda:externalDocument[@classCode='DOC']/cda:setId/@root"
93
+ end
94
+
95
+ #finds the node index of the first entry element in the measure template
96
+ def first_entry
97
+ "count(//cda:entry[cda:organizer[./cda:templateId[@root='#{@template_oid}']]" +
98
+ "/cda:reference[@typeCode='REFR']/cda:externalDocument[@classCode='DOC']" +
99
+ "/cda:id[@root='2.16.840.1.113883.4.738']][1]/preceding-sibling::*)+1"
100
+ end
101
+
102
+ #finds the node index of the extry that the specified setId is in, index is used if the same setId appears twice
103
+ def location_of_set_id(set_id,index)
104
+ "count(//cda:entry[cda:organizer[./cda:templateId[@root='#{@template_oid}']]" +
105
+ "/cda:reference[@typeCode='REFR']/cda:externalDocument[@classCode='DOC']" +
106
+ "/cda:setId[@root[contains(translate(.,'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLOMNOPQRSTUVWXYZ')" +
107
+ ",'#{set_id}')]]][#{index}]/preceding-sibling::*)+1"
108
+ end
109
+
110
+
111
+ def measure_population_selector
112
+ "/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
113
+ "/cda:organizer[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']]/cda:component" +
114
+ "/cda:observation[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.5']]/cda:reference" +
115
+ "/cda:externalObservation/cda:id/@root"
116
+ end
117
+
118
+ def find_measure_node_for_population(id)
119
+ "/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
120
+ "/cda:organizer[ ./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']" +
121
+ "and ./cda:component/cda:observation[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.5']]/cda:reference" +
122
+ "/cda:externalObservation/cda:id[@root='#{id.upcase}']]"
123
+ end
124
+ end
125
+ end
126
+
127
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "reported_result_extractor"
2
+ module HealthDataStandards
3
+ module Validate
4
+ class PerformanceRateValidator
5
+ include ReportedResultExtractor
6
+ include BaseValidator
7
+
8
+
9
+ def initialize()
10
+
11
+
12
+
13
+ end
14
+
15
+ # Nothing to see here - Move along
16
+ def validate(file, data = {})
17
+ errorsList = []
18
+ document = get_document(file)
19
+ #grab measure IDs from QRDA file
20
+ measure_ids = document.xpath(measure_selector).map(&:value).map(&:upcase)
21
+ measure_ids.each do |measure_id|
22
+ measures = HealthDataStandards::CQM::Measure.where(id: measure_id)
23
+ measures.each do |measure|
24
+ result_key = measure["population_ids"].dup
25
+ reported_result, errors = extract_results_by_ids(measure['id'], result_key, document)
26
+ #only check performace rate when there is one
27
+ if reported_result['PR'] != nil
28
+ error = check_performance_rates(reported_result, result_key, measure['id'], data)
29
+ if error != nil
30
+ errorsList << error
31
+ end
32
+ end
33
+ end
34
+ end
35
+ errorsList
36
+ end
37
+
38
+ def calculate_performance_rates(reported_result)
39
+ #Just in case a measure does not report these populations
40
+ denex = 0
41
+ denexcep = 0
42
+ denom = 0
43
+ numer = 0
44
+ if reported_result['DENEX'] != nil
45
+ denex = reported_result['DENEX']
46
+ end
47
+ if reported_result['DENEXCEP'] != nil
48
+ denexcep = reported_result['DENEXCEP']
49
+ end
50
+ if reported_result['DENOM'] != nil
51
+ denom = reported_result['DENOM']
52
+ end
53
+ if reported_result['NUMER'] != nil
54
+ numer = reported_result['NUMER']
55
+ end
56
+ denom = denom - denex - denexcep
57
+ pr = 0
58
+ if denom == 0
59
+ pr = "NA"
60
+ else
61
+ pr = numer / denom.to_f
62
+ end
63
+ return pr
64
+ end
65
+
66
+ def check_performance_rates(reported_result, population_ids, measure_id, data = {})
67
+ expected = calculate_performance_rates(reported_result)
68
+ _ids = population_ids
69
+ if expected == "NA"
70
+ if reported_result['PR']['nullFlavor'] != "NA"
71
+ return build_error("Reported Performance Rate for Numerator #{_ids['NUMER']} should be NA", "/", data[:file_name])
72
+ end
73
+ else
74
+ if reported_result['PR']['nullFlavor'] == "NA"
75
+ return build_error("Reported Performance Rate for Numerator #{_ids['NUMER']} should not be NA", "/", data[:file_name])
76
+ else
77
+ if (reported_result['PR']['value'].split('.',2).last.size > 6)
78
+ return build_error("Reported Performance Rate SHALL not have a precision greater than .000001 ", "/", data[:file_name])
79
+ elsif (reported_result['PR']['value'].to_f - expected.round(6)).abs > 0.0000001
80
+ return build_error("Reported Performance Rate of #{reported_result['PR']['value']} for Numerator #{_ids['NUMER']} does not match expected value of #{expected.to_s[0,8]}.", "/", data[:file_name])
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def measure_selector
87
+ "/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section/cda:entry" +
88
+ "/cda:organizer[./cda:templateId[@root='2.16.840.1.113883.10.20.27.3.1']]/cda:reference[@typeCode='REFR']" +
89
+ "/cda:externalDocument[@classCode='DOC']/cda:id[@root='2.16.840.1.113883.4.738']/@extension"
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,170 @@
1
+ module HealthDataStandards
2
+ module Validate
3
+ module ReportedResultExtractor
4
+
5
+ #takes a document and a list of 1 or more id hashes, e.g.:
6
+ #[{measure_id:"8a4d92b2-36af-5758-0136-ea8c43244986", set_id:"03876d69-085b-415c-ae9d-9924171040c2", ipp:"D77106C4-8ED0-4C5D-B29E-13DBF255B9FF", den:"8B0FA80F-8FFE-494C-958A-191C1BB36DBF", num:"9363135E-A816-451F-8022-96CDA7E540DD"}]
7
+ #returns nil if nothing matching is found
8
+ # returns a hash with the values of the populations filled out along with the population_ids added to the result
9
+
10
+
11
+ def extract_results_by_ids(measure_id, ids, doc)
12
+ results = nil
13
+ _ids = ids.dup
14
+ stratification = _ids.delete("stratification")
15
+ stratification ||= _ids.delete("STRAT")
16
+ errors = []
17
+ nodes = find_measure_node(measure_id, doc)
18
+
19
+ if nodes.nil? || nodes.empty?
20
+ # short circuit and return nil
21
+ return {}
22
+ end
23
+
24
+ nodes.each do |n|
25
+ results = get_measure_components(n, _ids, stratification)
26
+ break if (results != nil || (results != nil && !results.empty?))
27
+ end
28
+ return nil if results.nil?
29
+ results[:population_ids] = ids.dup
30
+ results
31
+ end
32
+
33
+ def find_measure_node(id, doc)
34
+ xpath_measures = %Q{/cda:ClinicalDocument/cda:component/cda:structuredBody/cda:component/cda:section
35
+ /cda:entry/cda:organizer[ ./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.1"]
36
+ and ./cda:reference/cda:externalDocument/cda:id[#{translate("@extension")}='#{id.upcase}' and #{translate("@root")}='2.16.840.1.113883.4.738']]}
37
+ return doc.xpath(xpath_measures)
38
+ end
39
+
40
+ def get_measure_components(n,ids, stratification)
41
+ results = {:supplemental_data =>{}}
42
+ ids.each_pair do |k,v|
43
+ val = nil
44
+ sup = nil
45
+ if (k == 'OBSERV')
46
+ msrpopl = ids['MSRPOPL']
47
+ val, sup = extract_cv_value(n,v,msrpopl, stratification)
48
+ else
49
+ val,sup,pr =extract_component_value(n,k,v,stratification)
50
+ end
51
+ if !val.nil?
52
+ results[k.to_s] = val
53
+ results[:supplemental_data][k] = sup
54
+ else
55
+ # return nil
56
+ end
57
+ if !pr.nil?
58
+ results["PR"] = pr
59
+ end
60
+ end
61
+ results
62
+ end
63
+
64
+ def extract_cv_value(node, id, msrpopl, strata = nil)
65
+ xpath_observation = %{ cda:component/cda:observation[./cda:value[@code = "MSRPOPL"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{msrpopl.upcase}']]}
66
+ cv = node.at_xpath(xpath_observation)
67
+ return nil unless cv
68
+ val = nil
69
+ if strata
70
+ strata_path = %{ cda:entryRelationship[@typeCode="COMP"]/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.4"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{strata.upcase}']]}
71
+ n = cv.xpath(strata_path)
72
+ val = get_cv_value(n,id)
73
+ else
74
+ val = get_cv_value(cv,id)
75
+ end
76
+ return val, (strata.nil? ? extract_supplemental_data(cv) : nil)
77
+ end
78
+
79
+ def extract_component_value(node, code, id, strata = nil)
80
+ xpath_observation = %{ cda:component/cda:observation[./cda:value[@code = "#{code}"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{id.upcase}']]}
81
+ cv = node.at_xpath(xpath_observation)
82
+ return nil unless cv
83
+ val = nil
84
+ if strata
85
+ strata_path = %{ cda:entryRelationship[@typeCode="COMP"]/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.4"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{strata.upcase}']]}
86
+ n = cv.xpath(strata_path)
87
+ val = get_aggregate_count(n) if n
88
+ else
89
+ val = get_aggregate_count(cv)
90
+ end
91
+ #Performance rate is only applicable for unstratified values
92
+ if code == "NUMER" && strata == nil
93
+ pref_rate_value = extract_performance_rate(node,code,id)
94
+ end
95
+ return val,(strata.nil? ? extract_supplemental_data(cv) : nil),pref_rate_value
96
+ end
97
+
98
+ def extract_performance_rate(node,code,id)
99
+ xpath_perf_rate = %{ cda:component/cda:observation[./cda:templateId[@root = "2.16.840.1.113883.10.20.27.3.14"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{id.upcase}']]/cda:value}
100
+ perf_rate = node.at_xpath(xpath_perf_rate)
101
+ pref_rate_value = {}
102
+ if perf_rate != nil
103
+ if perf_rate.at_xpath("./@nullFlavor")
104
+ pref_rate_value["nullFlavor"] = "NA"
105
+ return pref_rate_value
106
+ else
107
+ pref_rate_value["value"] = perf_rate.at_xpath("./@value").value
108
+ return pref_rate_value
109
+ end
110
+ end
111
+ return nil
112
+ end
113
+ # convert numbers in value nodes to Int / Float as necessary TODO add more types other than 'REAL'
114
+ def convert_value(value_node)
115
+ if value_node.nil?
116
+ return
117
+ end
118
+ if value_node['type'] == 'REAL' || value_node['value'].include?('.')
119
+ return value_node['value'].to_f
120
+ else
121
+ return value_node['value'].to_i
122
+ end
123
+ end
124
+
125
+ #given an observation node with an aggregate count node, return the reported and expected value within the count node
126
+ def get_cv_value(node, cv_id)
127
+ xpath_value = %{cda:entryRelationship/cda:observation[./cda:templateId[@root="2.16.840.1.113883.10.20.27.3.2"] and ./cda:reference/cda:externalObservation/cda:id[#{translate("@root")}='#{cv_id.upcase}']]/cda:value}
128
+
129
+ value_node = node.at_xpath(xpath_value)
130
+ value = convert_value(value_node) if value_node
131
+ value
132
+ end
133
+
134
+ #given an observation node with an aggregate count node, return the reported and expected value within the count node
135
+ def get_aggregate_count(node)
136
+ xpath_value = 'cda:entryRelationship/cda:observation[./cda:templateId[@root="2.16.840.1.113883.10.20.27.3.3"]]/cda:value'
137
+ value_node = node.at_xpath(xpath_value)
138
+ value = convert_value(value_node) if value_node
139
+ value
140
+ end
141
+
142
+ def extract_supplemental_data(cv)
143
+ ret = {}
144
+ supplemental_data_mapping = {"RACE"=> "2.16.840.1.113883.10.20.27.3.8",
145
+ "ETHNICITY" => "2.16.840.1.113883.10.20.27.3.7",
146
+ "SEX" => "2.16.840.1.113883.10.20.27.3.6",
147
+ "PAYER" => "2.16.840.1.113883.10.20.27.3.9"}
148
+ supplemental_data_mapping.each_pair do |supp, id|
149
+ key_hash = {}
150
+ xpath = "cda:entryRelationship/cda:observation[cda:templateId[@root='#{id}']]"
151
+ (cv.xpath(xpath) || []).each do |node|
152
+ value = node.at_xpath('cda:value')
153
+ count = get_aggregate_count(node)
154
+ if value.at_xpath("./@nullFlavor")
155
+ key_hash["UNK"] = count
156
+ else
157
+ key_hash[value['code']] = count
158
+ end
159
+ end
160
+ ret[supp.to_s] = key_hash
161
+ end
162
+ ret
163
+ end
164
+
165
+ def translate(id)
166
+ %{translate(#{id}, "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")}
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,24 @@
1
+ module HealthDataStandards
2
+ module Validate
3
+ module Schema
4
+ class Validator
5
+ include BaseValidator
6
+
7
+ def initialize(name, schema_file)
8
+ @name = name
9
+ @schema_file = schema_file
10
+ @xsd = Nokogiri::XML::Schema(File.new(@schema_file))
11
+ end
12
+
13
+ # Validate the document against the configured schema
14
+ def validate(document,data={})
15
+ @xsd.errors.clear
16
+ doc = get_document(document)
17
+ @xsd.validate(doc).map do |error|
18
+ build_error(error.message, "/", data[:file_name])
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ module HealthDataStandards
2
+ module Validate
3
+ module Schematron
4
+ module CProcessor
5
+
6
+
7
+ def get_errors(document)
8
+ document = get_document(document)
9
+ processor.transform(document)
10
+ end
11
+
12
+ def processor
13
+ return @processor if @processor
14
+ doc = Nokogiri::XML(File.open(@schematron_file))
15
+ doc.root["defaultPhase"] = ("errors")
16
+
17
+ xslt = Nokogiri::XSLT(File.open(ISO_SCHEMATRON))
18
+
19
+ result = xslt.transform(doc)
20
+ #this is stupid but needs to be done to assocaite the xslt file with a dirctory
21
+ result = Nokogiri::XML(result.to_s,@schematron_file)
22
+ @processor = Nokogiri::XSLT::Stylesheet.parse_stylesheet_doc(result)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,93 @@
1
+ require 'java'
2
+ require_relative '../../../../java/saxon9he.jar'
3
+
4
+ java_import "java.io.StringReader"
5
+ java_import "java.io.StringWriter"
6
+
7
+ java_import "javax.xml.parsers.DocumentBuilder"
8
+ java_import "javax.xml.parsers.DocumentBuilderFactory"
9
+
10
+ java_import "javax.xml.transform.TransformerFactory"
11
+ java_import "javax.xml.transform.Transformer"
12
+ java_import "javax.xml.transform.dom.DOMSource"
13
+ java_import "javax.xml.transform.stream.StreamSource"
14
+ java_import "javax.xml.transform.stream.StreamResult"
15
+
16
+ java_import "org.w3c.dom.Document"
17
+
18
+
19
+ TRANSFORMER_FACTORY_IMPL = "net.sf.saxon.TransformerFactoryImpl"
20
+
21
+ module HealthDataStandards
22
+ module Validate
23
+ module Schematron
24
+ module JavaProcessor
25
+
26
+ ISO_SCHEMATRON2 = File.join(DIR, 'resources/schematron/iso-schematron-xslt2/iso_svrl_for_xslt2.xsl')
27
+
28
+ class HdsUrlResolver
29
+ include javax.xml.transform.URIResolver
30
+
31
+ def initialize(schematron)
32
+ @file = schematron
33
+ end
34
+
35
+ def resolve(href, base)
36
+ path = File.join(File.dirname(@file), href)
37
+ return StreamSource.new(java.io.File.new(path))
38
+ end
39
+
40
+ end
41
+
42
+ def get_errors(document)
43
+ document_j = get_document_j(document)
44
+ output = build_transformer(StringReader.new(processor), StreamSource.new(document_j), true)
45
+ Nokogiri::XML(output)
46
+ end
47
+
48
+ def get_document_j(doc)
49
+ case doc
50
+ when File
51
+ java.io.File.new(doc.path)
52
+ else
53
+ StringReader.new(doc.to_s)
54
+ end
55
+ end
56
+
57
+ def processor
58
+ @processor ||= build_transformer(java.io.File.new(ISO_SCHEMATRON2), schematron_file)
59
+ end
60
+
61
+ def schematron_file
62
+ # this allows us to run the validation utility app in jBoss/TorqueBox
63
+ # for some reason it breaks the first time you call DocumentBuilderFactory,
64
+ # so the solution is to catch the error and retry
65
+ # TODO: pull this out when the above is no longer the case.
66
+ begin
67
+ dbf = DocumentBuilderFactory.new_instance
68
+ rescue Exception => ex
69
+ retry
70
+ end
71
+ dbf.setIgnoringElementContentWhitespace(true);
72
+ db = dbf.new_document_builder
73
+ document = db.parse(java.io.File.new(@schematron_file))
74
+
75
+ root = document.document_element
76
+ phase = root.set_attribute("defaultPhase", "errors")
77
+
78
+ DOMSource.new(root)
79
+ end
80
+
81
+ def build_transformer(xslt, input_file, url=false)
82
+ factory = TransformerFactory.newInstance(TRANSFORMER_FACTORY_IMPL, nil)
83
+ factory.uri_resolver = HdsUrlResolver.new(@schematron_file) if url
84
+ transformer = factory.new_transformer(StreamSource.new(xslt))
85
+ sw = StringWriter.new
86
+ output = StreamResult.new(sw)
87
+ transformer.transform(input_file, output)
88
+ sw.to_s
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end