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.
- 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
|