qa 3.1.0 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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