qa 3.1.0 → 4.0.0.rc1

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 (95) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +16 -548
  3. data/app/controllers/qa/linked_data_terms_controller.rb +64 -42
  4. data/app/controllers/qa/terms_controller.rb +14 -6
  5. data/app/models/qa/iri_template/url_config.rb +47 -0
  6. data/app/models/qa/iri_template/variable_map.rb +62 -0
  7. data/app/models/qa/linked_data/config/context_map.rb +77 -0
  8. data/app/models/qa/linked_data/config/context_property_map.rb +144 -0
  9. data/app/models/qa/linked_data/config/helper.rb +34 -0
  10. data/app/services/qa/iri_template_service.rb +31 -0
  11. data/{lib/qa/authorities → app/services/qa}/linked_data/authority_service.rb +3 -4
  12. data/app/services/qa/linked_data/authority_url_service.rb +48 -0
  13. data/app/services/qa/linked_data/deep_sort_service.rb +238 -0
  14. data/app/services/qa/linked_data/graph_service.rb +106 -0
  15. data/app/services/qa/linked_data/language_service.rb +30 -0
  16. data/app/services/qa/linked_data/language_sort_service.rb +81 -0
  17. data/app/services/qa/linked_data/mapper/context_mapper_service.rb +59 -0
  18. data/app/services/qa/linked_data/mapper/graph_mapper_service.rb +40 -0
  19. data/app/services/qa/linked_data/mapper/search_results_mapper_service.rb +70 -0
  20. data/config/authorities/linked_data/loc.json +5 -2
  21. data/config/authorities/linked_data/oclc_fast.json +3 -2
  22. data/config/initializers/linked_data_authorities.rb +1 -1
  23. data/config/locales/qa.en.yml +9 -0
  24. data/lib/generators/qa/install/templates/config/initializers/qa.rb +4 -0
  25. data/lib/qa.rb +8 -0
  26. data/lib/qa/authorities/assign_fast/generic_authority.rb +1 -1
  27. data/lib/qa/authorities/base.rb +0 -11
  28. data/lib/qa/authorities/crossref/generic_authority.rb +1 -1
  29. data/lib/qa/authorities/geonames.rb +1 -1
  30. data/lib/qa/authorities/getty/aat.rb +7 -2
  31. data/lib/qa/authorities/getty/tgn.rb +7 -2
  32. data/lib/qa/authorities/getty/ulan.rb +7 -2
  33. data/lib/qa/authorities/linked_data.rb +0 -1
  34. data/lib/qa/authorities/linked_data/config.rb +29 -28
  35. data/lib/qa/authorities/linked_data/config/search_config.rb +21 -79
  36. data/lib/qa/authorities/linked_data/config/term_config.rb +7 -77
  37. data/lib/qa/authorities/linked_data/find_term.rb +25 -17
  38. data/lib/qa/authorities/linked_data/generic_authority.rb +6 -5
  39. data/lib/qa/authorities/linked_data/rdf_helper.rb +6 -73
  40. data/lib/qa/authorities/linked_data/search_query.rb +54 -101
  41. data/lib/qa/authorities/loc/generic_authority.rb +4 -4
  42. data/lib/qa/authorities/web_service_base.rb +1 -8
  43. data/lib/qa/configuration.rb +7 -0
  44. data/lib/qa/version.rb +1 -1
  45. data/lib/tasks/mesh.rake +19 -18
  46. data/spec/controllers/linked_data_terms_controller_spec.rb +51 -1
  47. data/spec/controllers/terms_controller_spec.rb +15 -15
  48. data/spec/fixtures/authorities/linked_data/lod_encoding_config.json +2 -1
  49. data/spec/fixtures/authorities/linked_data/lod_full_config.json +56 -2
  50. data/spec/fixtures/authorities/linked_data/lod_full_config_1_0.json +164 -0
  51. data/spec/fixtures/authorities/linked_data/lod_lang_defaults.json +5 -4
  52. data/spec/fixtures/authorities/linked_data/lod_lang_multi_defaults.json +3 -2
  53. data/spec/fixtures/authorities/linked_data/lod_lang_no_defaults.json +3 -2
  54. data/spec/fixtures/authorities/linked_data/lod_lang_param.json +3 -2
  55. data/spec/fixtures/authorities/linked_data/lod_min_config.json +3 -2
  56. data/spec/fixtures/authorities/linked_data/lod_search_only_config.json +2 -1
  57. data/spec/fixtures/authorities/linked_data/lod_sort.json +2 -1
  58. data/spec/fixtures/authorities/linked_data/lod_term_id_param_config.json +2 -1
  59. data/spec/fixtures/authorities/linked_data/lod_term_only_config.json +2 -1
  60. data/spec/fixtures/authorities/linked_data/lod_term_uri_param_config.json +2 -1
  61. data/spec/fixtures/getty-error-response.txt +10 -0
  62. data/spec/fixtures/lod_2_ranked_2_unranked.nt +17 -0
  63. data/spec/fixtures/lod_3_ranked_varying_preds.nt +16 -0
  64. data/spec/fixtures/lod_lang_search_filtering.nt +11 -0
  65. data/spec/fixtures/lod_search_with_blanknode_subjects.nt +18 -0
  66. data/spec/fixtures/lod_term_with_blanknode_objects.nt +8 -0
  67. data/spec/lib/authorities/assign_fast_spec.rb +1 -0
  68. data/spec/lib/authorities/getty/aat_spec.rb +14 -2
  69. data/spec/lib/authorities/getty/tgn_spec.rb +14 -2
  70. data/spec/lib/authorities/getty/ulan_spec.rb +14 -2
  71. data/spec/lib/authorities/linked_data/authority_service_spec.rb +2 -1
  72. data/spec/lib/authorities/linked_data/config_spec.rb +284 -5
  73. data/spec/lib/authorities/linked_data/find_term_spec.rb +3 -1
  74. data/spec/lib/authorities/linked_data/generic_authority_spec.rb +92 -42
  75. data/spec/lib/authorities/linked_data/search_config_spec.rb +67 -160
  76. data/spec/lib/authorities/linked_data/search_query_spec.rb +3 -127
  77. data/spec/lib/authorities/linked_data/term_config_spec.rb +6 -134
  78. data/spec/lib/authorities/loc_spec.rb +9 -9
  79. data/spec/lib/configuration_spec.rb +20 -7
  80. data/spec/lib/tasks/mesh.rake_spec.rb +2 -2
  81. data/spec/models/iri_template/url_config_spec.rb +102 -0
  82. data/spec/models/iri_template/variable_map_spec.rb +105 -0
  83. data/spec/models/linked_data/config/context_map_spec.rb +148 -0
  84. data/spec/models/linked_data/config/context_property_map_spec.rb +286 -0
  85. data/spec/services/iri_template_service_spec.rb +69 -0
  86. data/spec/services/linked_data/authority_url_service_spec.rb +107 -0
  87. data/spec/services/linked_data/deep_sort_service_spec.rb +260 -0
  88. data/spec/services/linked_data/graph_service_spec.rb +232 -0
  89. data/spec/services/linked_data/language_service_spec.rb +66 -0
  90. data/spec/services/linked_data/language_sort_service_spec.rb +58 -0
  91. data/spec/services/linked_data/mapper/context_mapper_service_spec.rb +137 -0
  92. data/spec/services/linked_data/mapper/graph_mapper_service_spec.rb +110 -0
  93. data/spec/services/linked_data/mapper/search_results_mapper_service_spec.rb +109 -0
  94. data/spec/spec_helper.rb +10 -2
  95. metadata +81 -11
@@ -0,0 +1,34 @@
1
+ # Provide helper method for common processing of configurations.
2
+ module Qa
3
+ module LinkedData
4
+ module Config
5
+ class Helper
6
+ # Fetch a value from a hash map
7
+ def self.fetch(map, key, default)
8
+ map.fetch(key, default)
9
+ end
10
+
11
+ # Fetch a boolean value from a hash map throwing an exception if the value is not boolean
12
+ def self.fetch_boolean(map, key, default)
13
+ value = map.fetch(key, default)
14
+ raise Qa::InvalidConfiguration, "#{key} must be true or false" unless value == true || value == false
15
+ value
16
+ end
17
+
18
+ # Fetch a value from a hash map throwing an exception if the value is blank
19
+ def self.fetch_required(map, key, default)
20
+ value = map.fetch(key, default)
21
+ raise Qa::InvalidConfiguration, "#{key} is required" unless value
22
+ value
23
+ end
24
+
25
+ # Fetch a value from a hash map throwing an exception if the value is blank
26
+ def self.fetch_symbol(map, key, default)
27
+ value = map.fetch(key, default)
28
+ return value unless value.respond_to? :to_sym
29
+ value.to_sym
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # Provide service for building a URL based on an IRI Templated Link and its variable mappings based on provided substitutions.
2
+ module Qa
3
+ class IriTemplateService
4
+ # Construct an url from an IriTemplate making identified substitutions
5
+ # @param url_config [Qa::IriTemplate::UrlConfig] configuration (json) holding the template and variable mappings
6
+ # @param substitutions [HashWithIndifferentAccess] name-value pairs to substitute into the url template
7
+ # @return [String] url with substitutions
8
+ def self.build_url(url_config:, substitutions:)
9
+ # TODO: This is a very simple approach using direct substitution into the template string.
10
+ # Better would be to...
11
+ # * patterns without a substitution are not included in the resulting URL
12
+ # * appropriately adds '?' or '&'
13
+ # * ensure proper escaping of values (e.g. value="A simple string" which is encoded as A%20simple%20string)
14
+ # Even more advanced would be to...
15
+ # * support BasicRepresentation (which is what it does now)
16
+ # * support ExplicitRepresentation
17
+ # * literal encoding for values (e.g. value="A simple string" becomes %22A%20simple%20string%22)
18
+ # * language encoding for values (e.g. value="A simple string" becomes value="A simple string"@en which is encoded as %22A%20simple%20string%22%40en)
19
+ # * type encoding for values (e.g. value=5.5 becomes value="5.5"^^http://www.w3.org/2001/XMLSchema#decimal which is encoded
20
+ # as %225.5%22%5E%5Ehttp%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%23decimal)
21
+ # Fuller implementations parse the template into component parts and then build the URL by adding parts in as applicable.
22
+ url = url_config.template
23
+ url_config.mapping.each do |m|
24
+ key = m.variable
25
+ url = url.gsub("{#{key}}", m.simple_value(substitutions[key]))
26
+ url = url.gsub("{?#{key}}", m.parameter_value(substitutions[key]))
27
+ end
28
+ url
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,5 @@
1
- # This module has the primary QA search method. It also includes methods to process the linked data results and convert
2
- # them into the expected QA json results format.
3
- module Qa::Authorities
1
+ # This module loads linked data authorities and provides access to their configurations.
2
+ module Qa
4
3
  module LinkedData
5
4
  class AuthorityService
6
5
  # Load or reload the linked data configuration files
@@ -32,7 +31,7 @@ module Qa::Authorities
32
31
 
33
32
  # Get the configuration for an authority
34
33
  # @param [String] name of the authority
35
- # @return [Array<String>] configuration for the specified authority
34
+ # @return [Hash] configuration for the specified authority
36
35
  def self.authority_config(authname)
37
36
  authority_configs[authname]
38
37
  end
@@ -0,0 +1,48 @@
1
+ # Provide service for constructing the external access URL for an authority.
2
+ module Qa
3
+ module LinkedData
4
+ class AuthorityUrlService
5
+ class << self
6
+ # Build a url for an authority/subauthority for the specified action.
7
+ # @param authority [Symbol] name of a registered authority
8
+ # @param subauthority [String] name of a subauthority
9
+ # @param action [Symbol] action with valid values :search or :term
10
+ # @param action_request [String] the request the user is making of the authority (e.g. query text or term id/uri)
11
+ # @param substitutions [Hash] variable-value pairs to substitute into the URL template
12
+ # @return a valid URL the submits the action request to the external authority
13
+ def build_url(action_config:, action:, action_request:, substitutions: {}, subauthority: nil)
14
+ action_validation(action)
15
+ url_config = action_config.url_config
16
+ selected_substitutions = url_config.extract_substitutions(combined_substitutions(action_config, action, action_request, substitutions, subauthority))
17
+ Qa::IriTemplateService.build_url(url_config: url_config, substitutions: selected_substitutions)
18
+ end
19
+
20
+ private
21
+
22
+ def action_validation(action)
23
+ return if [:search, :term].include? action
24
+ raise Qa::UnsupportedAction, "#{action} Not Supported - Action must be one of the supported actions (e.g. :term, :search)"
25
+ end
26
+
27
+ def combined_substitutions(action_config, action, action_request, substitutions, subauthority)
28
+ substitutions[action_request_variable(action_config, action)] = action_request
29
+ substitutions[action_subauth_variable(action_config)] = action_subauth_variable_value(action_config, subauthority) if subauthority.present?
30
+ substitutions
31
+ end
32
+
33
+ def action_request_variable(action_config, action)
34
+ key = action == :search ? :query : :term_id
35
+ action_config.qa_replacement_patterns[key]
36
+ end
37
+
38
+ def action_subauth_variable(action_config)
39
+ action_config.qa_replacement_patterns[:subauth]
40
+ end
41
+
42
+ def action_subauth_variable_value(action_config, subauthority)
43
+ action_config.subauthorities[subauthority.to_sym]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,238 @@
1
+ # Provide service for for sorting an array of hash based on the values at a specified key in the hash.
2
+ module Qa
3
+ module LinkedData
4
+ class DeepSortService
5
+ # @params [Array<Hash<Symbol,Array<RDF::Literal>>>] the array of hashes to sort
6
+ # @params [sort_key] the key in the hash on whose value the array will be sorted
7
+ # @return instance of this class
8
+ # @example the_array parameter
9
+ # [
10
+ # {:uri=>[#<RDF::URI:0x3fcff54a829c URI:http://id.loc.gov/authorities/names/n2010043281>],
11
+ # :id=>[#<RDF::Literal:0x3fcff4a367b4("n 2010043281")>],
12
+ # :label=>[#<RDF::Literal:0x3fcff54a9a98("Valli, Sabrina"@en)>],
13
+ # :altlabel=>[],
14
+ # :sort=>[#<RDF::Literal:0x3fcff54b4c18("2")>]},
15
+ # {:uri=>[#<RDF::URI:0x3fcff54a829c URI:http://id.loc.gov/authorities/names/n201002344>],
16
+ # :id=>[#<RDF::Literal:0x3fcff4a367b4("n 201002344")>],
17
+ # :label=>[#<RDF::Literal:0x3fcff54a9a98("Cornell, Joseph"@en)>],
18
+ # :altlabel=>[],
19
+ # :sort=>[#<RDF::Literal:0x3fcff54b4c18("1")>]}
20
+ # ]
21
+ def initialize(the_array, sort_key, preferred_language = nil)
22
+ @sortable_elements = the_array.map { |element| DeepSortElement.new(element, sort_key, preferred_language) }
23
+ end
24
+
25
+ # Sort an array of hash on the specified sort key. The value in the hash at sort key is expected to be an array
26
+ # with one or more values that are RDF::Literals that translate to a number (e.g. 2), a string number (e.g. "3"),
27
+ # a string (e.g. "hello"), or a language qualified string (e.g. "hello"@en).
28
+ # The sort occurs in the following precedence.
29
+ # * preference for numeric sort (if only one value each and both are integers or a string that can be converted to an integer)
30
+ # * single value sort (if only one value each and at least one is not an integer)
31
+ # * multiple values sort (if either has multiple values)
32
+ # @return the sorted array
33
+ # @example returned sorted array
34
+ # [
35
+ # {:uri=>[#<RDF::URI:0x3fcff54a829c URI:http://id.loc.gov/authorities/names/n201002344>],
36
+ # :id=>[#<RDF::Literal:0x3fcff4a367b4("n 201002344")>],
37
+ # :label=>[#<RDF::Literal:0x3fcff54a9a98("Cornell, Joseph"@en)>],
38
+ # :altlabel=>[],
39
+ # :sort=>[#<RDF::Literal:0x3fcff54b4c18("1")>]},
40
+ # {:uri=>[#<RDF::URI:0x3fcff54a829c URI:http://id.loc.gov/authorities/names/n2010043281>],
41
+ # :id=>[#<RDF::Literal:0x3fcff4a367b4("n 2010043281")>],
42
+ # :label=>[#<RDF::Literal:0x3fcff54a9a98("Valli, Sabrina"@en)>],
43
+ # :altlabel=>[],
44
+ # :sort=>[#<RDF::Literal:0x3fcff54b4c18("2")>]}
45
+ # ]
46
+ def sort
47
+ @sortable_elements.sort.map(&:element)
48
+ end
49
+
50
+ class DeepSortElement
51
+ attr_reader :element, :literals, :preferred_language
52
+ private :preferred_language
53
+
54
+ delegate :size, to: :@literals
55
+
56
+ def initialize(element, sort_key, preferred_language)
57
+ element[sort_key] = Qa::LinkedData::LanguageSortService.new(element[sort_key], preferred_language).sort
58
+ @element = element
59
+ @literals = element[sort_key]
60
+ @preferred_language = preferred_language
61
+ @includes_preferred_language = includes_preferred_language?
62
+ @all_same_language = all_same_language?
63
+ end
64
+
65
+ def <=>(other)
66
+ return numeric_comparator(other) if integer? && other.integer?
67
+ return single_value_comparator(other) if single? && other.single?
68
+ multiple_value_comparator(other)
69
+ end
70
+
71
+ # @return true if there is a single literal that is an integer or a string that can be converted to an integer; otherwise, false
72
+ def integer?
73
+ return false unless single?
74
+ (/\A[-+]?\d+\z/ === literal.to_s) # rubocop:disable Style/CaseEquality
75
+ end
76
+
77
+ def integer(idx = 0)
78
+ Integer(literal(idx).to_s)
79
+ end
80
+
81
+ # @return true if there is only one value; otherwise, false
82
+ def single?
83
+ @single ||= literals.size == 1
84
+ end
85
+
86
+ def literal(idx = 0)
87
+ literals[idx]
88
+ end
89
+
90
+ def downcase_string(idx = 0)
91
+ to_downcase(literal(idx))
92
+ end
93
+
94
+ def language(lit = literals.first)
95
+ return nil unless Qa::LinkedData::LanguageService.literal_has_language_marker? lit
96
+ lit.language
97
+ end
98
+
99
+ def includes_preferred_language?
100
+ return @includes_preferred_language if @includes_preferred_language.present?
101
+ # literals are sorted by language with preferred language first in the list
102
+ @includes_preferred_language = (language == preferred_language)
103
+ end
104
+
105
+ def all_same_language?
106
+ return @all_same_language if @all_same_language.present?
107
+ # literals are sorted by language, so if first = last, then all are the same
108
+ @all_same_language = (language(literals.first) == language(literals.last))
109
+ end
110
+
111
+ def languages
112
+ filtered_literals_by_language.keys
113
+ end
114
+
115
+ def filtered_literals(filter_language)
116
+ filtered_literals_by_language.fetch(filter_language, [])
117
+ end
118
+
119
+ private
120
+
121
+ # If both test values are single value and both are integers, do a numeric sort
122
+ def numeric_comparator(other)
123
+ integer <=> other.integer
124
+ end
125
+
126
+ # If both test values are single value and at least one is not numeric, do a string sort taking language into consideration
127
+ # * sort values if neither has a language marker or they both have the same language marker
128
+ # * otherwise, sort language markers
129
+ def single_value_comparator(other)
130
+ return downcase_string <=> other.downcase_string if same_language?(literal, other.literal)
131
+ compare_languages(language, other.language)
132
+ end
133
+
134
+ def compare_languages(lang, other_lang)
135
+ return -1 if preferred_language? lang
136
+ return 1 if preferred_language? other_lang
137
+ return -1 if other_lang.blank?
138
+ return 1 if lang.blank?
139
+ lang <=> other_lang
140
+ end
141
+
142
+ # If at least one of the test values has multiple values, sort the multiple values taking language into consideration
143
+ # * if both lists have all the same language or no language markers at all, just sort the lists and compare each element
144
+ # * if either list has the preferred language, try to sort the two lists by element after filtering for the preferred language
145
+ # * otherwise, sort by language until there is a difference
146
+ def multiple_value_comparator(other)
147
+ return single_language_list_comparator(other) if all_same_language? && other.all_same_language?
148
+ return specified_language_list_comparator(other, preferred_language) if includes_preferred_language? && other.includes_preferred_language?
149
+ multi_language_list_comparator(other)
150
+ end
151
+
152
+ def single_language_list_comparator(other)
153
+ list_comparator(literals, other.literals)
154
+ end
155
+
156
+ def specified_language_list_comparator(other, lang)
157
+ filtered = filtered_literals(lang)
158
+ other_filtered = other.filtered_literals(lang)
159
+ return -1 if !filtered.empty? && other_filtered.empty?
160
+ return 1 if filtered.empty? && !other_filtered.empty?
161
+ list_comparator(filtered, other_filtered)
162
+ end
163
+
164
+ # Walk through language sorted lists
165
+ # * for each language, determine how closely the list of terms matches
166
+ # * prioritize the list that gets the most low values
167
+ def multi_language_list_comparator(other)
168
+ combined_languages = languages.concat(other.languages).uniq
169
+ by_language_comparisons = {}
170
+ combined_languages.each do |lang|
171
+ cmp = list_comparator(filtered_literals(lang), other.filtered_literals(lang))
172
+ by_language_comparisons[lang] = cmp
173
+ end
174
+ cmp_sum = by_language_comparisons.values.sum
175
+ return 1 if cmp_sum.positive?
176
+ return -1 if cmp_sum.negative?
177
+ 0
178
+ end
179
+
180
+ def list_comparator(list, other_list)
181
+ # if an element doesn't have any terms in a language, the other element sorts lower
182
+ return -1 if other_list.empty?
183
+ return 1 if list.empty?
184
+ shorter_list_size = [list.size, other_list.size].min
185
+ cmp = 0
186
+ 0.upto(shorter_list_size - 1) do |idx|
187
+ cmp = to_downcase(list[idx]) <=> to_downcase(other_list[idx])
188
+ return cmp unless cmp.zero?
189
+ end
190
+ return cmp if list.size == other_list.size
191
+ other_list.size < list.size ? 1 : -1 # didn't find any diffs, shorter list is considered lower
192
+ end
193
+
194
+ def same_language?(lit, other_lit)
195
+ return false if only_one_has_language_marker?(lit, other_lit)
196
+ return true if neither_have_language_markers?(lit, other_lit)
197
+ lit.language == other_lit.language
198
+ end
199
+
200
+ def neither_have_language_markers?(lit, other_lit)
201
+ !language?(lit) && !language?(other_lit)
202
+ end
203
+
204
+ def only_one_has_language_marker?(lit, other_lit)
205
+ (!language?(lit) && language?(other_lit)) || (language?(lit) && !language?(other_lit))
206
+ end
207
+
208
+ def language?(lit)
209
+ Qa::LinkedData::LanguageService.literal_has_language_marker? lit
210
+ end
211
+
212
+ def preferred_language?(lang)
213
+ preferred_language.present? ? lang == preferred_language : false
214
+ end
215
+
216
+ def to_downcase(lit)
217
+ lit.to_s.downcase
218
+ end
219
+
220
+ def filtered_literals_by_language
221
+ @filtered_literals_by_language ||= create_all_filters
222
+ end
223
+
224
+ def create_all_filters
225
+ bins = {}
226
+ 0.upto(size - 1) do |idx|
227
+ lang = language(literals[idx])
228
+ filter = bins.fetch(lang, [])
229
+ filter << literal(idx)
230
+ bins[lang] = filter
231
+ end
232
+ bins
233
+ end
234
+ end
235
+ private_constant :DeepSortElement
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,106 @@
1
+ # Extend the RDF graph to include additional processing methods.
2
+ module Qa
3
+ module LinkedData
4
+ class GraphService
5
+ class << self
6
+ # Retrieve linked data from specified url
7
+ # @param [String] url from which to retrieve linked data
8
+ # @return [RDF::Graph] graph of linked data
9
+ def load_graph(url:)
10
+ RDF::Graph.load(url)
11
+ rescue IOError => e
12
+ process_error(e, url)
13
+ end
14
+
15
+ # Create a new graph with statements filtered out
16
+ # @param graph [RDF::Graph] the original graph to be filtered
17
+ # @param language [Array<Symbol>] will keep any statement whose object's language matches the language filter
18
+ # (only applies to statements that respond to language) (e.g. [:fr] or [:en, :fr])
19
+ # @param remove_blanknode_subjects [Boolean] will remove any statement whose subject is a blanknode, if true
20
+ # @return [RDF::Graph] a new instance of graph with statements not matching the filters removed
21
+ def filter(graph:, language: nil, remove_blanknode_subjects: false)
22
+ return graph unless graph.present?
23
+ return graph unless language.present? || remove_blanknode_subjects
24
+ filtered_graph = deep_copy(graph: graph)
25
+ filtered_graph.statements.each do |st|
26
+ filtered_graph.delete(st) if filter_out_blanknode(remove_blanknode_subjects, st.subject) || filter_out_language(graph, language, st)
27
+ end
28
+ filtered_graph
29
+ end
30
+
31
+ # Get object values from the graph that have the subject-predicate.
32
+ # @param graph [RDF::Graph] the graph to search
33
+ # @param subject [RDF::URI] the URI of the subject
34
+ # @param predicate [RDF::URI] the URI of the predicate
35
+ # @return [Array] all object values for the subject-predicate pair
36
+ def object_values(graph:, subject:, predicate:)
37
+ values = []
38
+ graph.query([subject, predicate, :object]) do |statement|
39
+ values << statement.object
40
+ end
41
+ values
42
+ end
43
+
44
+ def deep_copy(graph:)
45
+ new_graph = RDF::Graph.new
46
+ graph.statements.each do |st|
47
+ new_graph.insert(st.dup)
48
+ end
49
+ new_graph
50
+ end
51
+
52
+ private
53
+
54
+ def filter_out_blanknode(remove, subj)
55
+ remove && subj.anonymous?
56
+ end
57
+
58
+ # Filter out language based on...
59
+ # * do not remove if the object literal does not respond to :language
60
+ # * do not remove if the object literal does not have a language marker
61
+ # * do not remove if the object has the targeted language marker
62
+ # * do not remove if none of the other objects with this statement's predicate have the targeted language marker
63
+ def filter_out_language(graph, language, statement)
64
+ return false if language.blank?
65
+ return false unless Qa::LinkedData::LanguageService.literal_has_language_marker?(statement.object)
66
+ objects = object_values(graph: graph, subject: statement.subject, predicate: statement.predicate)
67
+ return false unless at_least_one_object_has_language?(objects, language)
68
+ !language.include?(statement.object.language)
69
+ end
70
+
71
+ def at_least_one_object_has_language?(objects, language)
72
+ objects.each do |obj|
73
+ next unless Qa::LinkedData::LanguageService.literal_has_language_marker?(obj)
74
+ next unless language.include? obj.language
75
+ return true
76
+ end
77
+ false
78
+ end
79
+
80
+ def process_error(e, url)
81
+ uri = URI(url)
82
+ raise RDF::FormatError, "Unknown RDF format of results returned by #{uri}. (RDF::FormatError) You may need to include gem 'linkeddata'." if e.is_a? RDF::FormatError
83
+ response_code = ioerror_code(e)
84
+ case response_code
85
+ when '404'
86
+ raise Qa::TermNotFound, "#{uri} Not Found - Term may not exist at LOD Authority. (HTTPNotFound - 404)"
87
+ when '500'
88
+ raise Qa::ServiceError, "#{uri.hostname} on port #{uri.port} is not responding. Try again later. (HTTPServerError - 500)"
89
+ when '503'
90
+ raise Qa::ServiceUnavailable, "#{uri.hostname} on port #{uri.port} is not responding. Try again later. (HTTPServiceUnavailable - 503)"
91
+ else
92
+ raise Qa::ServiceError, "Unknown error for #{uri.hostname} on port #{uri.port}. Try again later. (Cause - #{e.message})"
93
+ end
94
+ end
95
+
96
+ def ioerror_code(e)
97
+ msg = e.message
98
+ return 'format' if msg.start_with? "Unknown RDF format"
99
+ a = msg.size - 4
100
+ z = msg.size - 2
101
+ msg[a..z]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end