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.
- checksums.yaml +5 -5
- data/README.md +16 -548
- data/app/controllers/qa/linked_data_terms_controller.rb +64 -42
- data/app/controllers/qa/terms_controller.rb +14 -6
- data/app/models/qa/iri_template/url_config.rb +47 -0
- data/app/models/qa/iri_template/variable_map.rb +62 -0
- data/app/models/qa/linked_data/config/context_map.rb +77 -0
- data/app/models/qa/linked_data/config/context_property_map.rb +144 -0
- data/app/models/qa/linked_data/config/helper.rb +34 -0
- data/app/services/qa/iri_template_service.rb +31 -0
- data/{lib/qa/authorities → app/services/qa}/linked_data/authority_service.rb +3 -4
- data/app/services/qa/linked_data/authority_url_service.rb +48 -0
- data/app/services/qa/linked_data/deep_sort_service.rb +238 -0
- data/app/services/qa/linked_data/graph_service.rb +106 -0
- data/app/services/qa/linked_data/language_service.rb +30 -0
- data/app/services/qa/linked_data/language_sort_service.rb +81 -0
- data/app/services/qa/linked_data/mapper/context_mapper_service.rb +59 -0
- data/app/services/qa/linked_data/mapper/graph_mapper_service.rb +40 -0
- data/app/services/qa/linked_data/mapper/search_results_mapper_service.rb +70 -0
- data/config/authorities/linked_data/loc.json +5 -2
- data/config/authorities/linked_data/oclc_fast.json +3 -2
- data/config/initializers/linked_data_authorities.rb +1 -1
- data/config/locales/qa.en.yml +9 -0
- data/lib/generators/qa/install/templates/config/initializers/qa.rb +4 -0
- data/lib/qa.rb +8 -0
- data/lib/qa/authorities/assign_fast/generic_authority.rb +1 -1
- data/lib/qa/authorities/base.rb +0 -11
- data/lib/qa/authorities/crossref/generic_authority.rb +1 -1
- data/lib/qa/authorities/geonames.rb +1 -1
- data/lib/qa/authorities/getty/aat.rb +7 -2
- data/lib/qa/authorities/getty/tgn.rb +7 -2
- data/lib/qa/authorities/getty/ulan.rb +7 -2
- data/lib/qa/authorities/linked_data.rb +0 -1
- data/lib/qa/authorities/linked_data/config.rb +29 -28
- data/lib/qa/authorities/linked_data/config/search_config.rb +21 -79
- data/lib/qa/authorities/linked_data/config/term_config.rb +7 -77
- data/lib/qa/authorities/linked_data/find_term.rb +25 -17
- data/lib/qa/authorities/linked_data/generic_authority.rb +6 -5
- data/lib/qa/authorities/linked_data/rdf_helper.rb +6 -73
- data/lib/qa/authorities/linked_data/search_query.rb +54 -101
- data/lib/qa/authorities/loc/generic_authority.rb +4 -4
- data/lib/qa/authorities/web_service_base.rb +1 -8
- data/lib/qa/configuration.rb +7 -0
- data/lib/qa/version.rb +1 -1
- data/lib/tasks/mesh.rake +19 -18
- data/spec/controllers/linked_data_terms_controller_spec.rb +51 -1
- data/spec/controllers/terms_controller_spec.rb +15 -15
- data/spec/fixtures/authorities/linked_data/lod_encoding_config.json +2 -1
- data/spec/fixtures/authorities/linked_data/lod_full_config.json +56 -2
- data/spec/fixtures/authorities/linked_data/lod_full_config_1_0.json +164 -0
- data/spec/fixtures/authorities/linked_data/lod_lang_defaults.json +5 -4
- data/spec/fixtures/authorities/linked_data/lod_lang_multi_defaults.json +3 -2
- data/spec/fixtures/authorities/linked_data/lod_lang_no_defaults.json +3 -2
- data/spec/fixtures/authorities/linked_data/lod_lang_param.json +3 -2
- data/spec/fixtures/authorities/linked_data/lod_min_config.json +3 -2
- data/spec/fixtures/authorities/linked_data/lod_search_only_config.json +2 -1
- data/spec/fixtures/authorities/linked_data/lod_sort.json +2 -1
- data/spec/fixtures/authorities/linked_data/lod_term_id_param_config.json +2 -1
- data/spec/fixtures/authorities/linked_data/lod_term_only_config.json +2 -1
- data/spec/fixtures/authorities/linked_data/lod_term_uri_param_config.json +2 -1
- data/spec/fixtures/getty-error-response.txt +10 -0
- data/spec/fixtures/lod_2_ranked_2_unranked.nt +17 -0
- data/spec/fixtures/lod_3_ranked_varying_preds.nt +16 -0
- data/spec/fixtures/lod_lang_search_filtering.nt +11 -0
- data/spec/fixtures/lod_search_with_blanknode_subjects.nt +18 -0
- data/spec/fixtures/lod_term_with_blanknode_objects.nt +8 -0
- data/spec/lib/authorities/assign_fast_spec.rb +1 -0
- data/spec/lib/authorities/getty/aat_spec.rb +14 -2
- data/spec/lib/authorities/getty/tgn_spec.rb +14 -2
- data/spec/lib/authorities/getty/ulan_spec.rb +14 -2
- data/spec/lib/authorities/linked_data/authority_service_spec.rb +2 -1
- data/spec/lib/authorities/linked_data/config_spec.rb +284 -5
- data/spec/lib/authorities/linked_data/find_term_spec.rb +3 -1
- data/spec/lib/authorities/linked_data/generic_authority_spec.rb +92 -42
- data/spec/lib/authorities/linked_data/search_config_spec.rb +67 -160
- data/spec/lib/authorities/linked_data/search_query_spec.rb +3 -127
- data/spec/lib/authorities/linked_data/term_config_spec.rb +6 -134
- data/spec/lib/authorities/loc_spec.rb +9 -9
- data/spec/lib/configuration_spec.rb +20 -7
- data/spec/lib/tasks/mesh.rake_spec.rb +2 -2
- data/spec/models/iri_template/url_config_spec.rb +102 -0
- data/spec/models/iri_template/variable_map_spec.rb +105 -0
- data/spec/models/linked_data/config/context_map_spec.rb +148 -0
- data/spec/models/linked_data/config/context_property_map_spec.rb +286 -0
- data/spec/services/iri_template_service_spec.rb +69 -0
- data/spec/services/linked_data/authority_url_service_spec.rb +107 -0
- data/spec/services/linked_data/deep_sort_service_spec.rb +260 -0
- data/spec/services/linked_data/graph_service_spec.rb +232 -0
- data/spec/services/linked_data/language_service_spec.rb +66 -0
- data/spec/services/linked_data/language_sort_service_spec.rb +58 -0
- data/spec/services/linked_data/mapper/context_mapper_service_spec.rb +137 -0
- data/spec/services/linked_data/mapper/graph_mapper_service_spec.rb +110 -0
- data/spec/services/linked_data/mapper/search_results_mapper_service_spec.rb +109 -0
- data/spec/spec_helper.rb +10 -2
- 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
|
2
|
-
|
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 [
|
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
|