qa 2.1.2 → 2.2.0

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.
@@ -3,7 +3,7 @@
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
- <title>QA 2.1 Linked Data API</title>
6
+ <title>QA 2.2 Linked Data API</title>
7
7
  <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
8
8
  <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
9
9
  <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
@@ -4,4 +4,10 @@ Qa.config do |config|
4
4
  # More information on CORS headers at: https://fetch.spec.whatwg.org/#cors-protocol
5
5
  # config.enable_cors_headers
6
6
  # config.disable_cors_headers
7
+
8
+ # Provide a token that allows reloading of linked data authorities through the controller
9
+ # action '/reload/linked_data/authorities?auth_token=YOUR_AUTH_TOKEN_DEFINED_HERE' without
10
+ # requiring a restart of rails. By default, reloading through the browser is not allowed
11
+ # when the token is nil or blank. Change to any string to control who has access to reload.
12
+ # config.authorized_reload_token = YOUR_AUTH_TOKEN_DEFINED_HERE
7
13
  end
@@ -10,16 +10,23 @@ module Qa::Authorities
10
10
  "http://vocab.getty.edu/sparql.json?query=#{URI.escape(sparql(q))}&_implicit=false&implicit=true&_equivalent=false&_form=%2Fsparql"
11
11
  end
12
12
 
13
- def sparql(q)
13
+ def sparql(q) # rubocop:disable Metrics/MethodLength
14
14
  search = untaint(q)
15
+ if search.include?(' ')
16
+ clauses = search.split(' ').collect do |i|
17
+ %((regex(?name, "#{i}", "i")))
18
+ end
19
+ ex = "(#{clauses.join(' && ')})"
20
+ else
21
+ ex = %(regex(?name, "#{search}", "i"))
22
+ end
15
23
  # The full text index matches on fields besides the term, so we filter to ensure the match is in the term.
16
- sparql = "SELECT ?s ?name {
17
- ?s a skos:Concept; luc:term \"#{search}\";
24
+ %(SELECT ?s ?name {
25
+ ?s a skos:Concept; luc:term "#{search}";
18
26
  skos:inScheme <http://vocab.getty.edu/aat/> ;
19
27
  gvp:prefLabelGVP [skosxl:literalForm ?name].
20
- FILTER regex(?name, \"#{search}\", \"i\") .
21
- } ORDER BY ?name"
22
- sparql
28
+ FILTER #{ex} .
29
+ } ORDER BY ?name).gsub(/[\s\n]+/, " ")
23
30
  end
24
31
 
25
32
  def untaint(q)
@@ -19,31 +19,20 @@ module Qa::Authorities
19
19
  def sparql(q) # rubocop:disable Metrics/MethodLength
20
20
  search = untaint(q)
21
21
  if search.include?(' ')
22
- ex = "(("
23
- search.split(' ').each do |i|
24
- ex += "regex(CONCAT(?name, ', ', REPLACE(str(?par), \",[^,]+,[^,]+$\", \"\")), \"#{i}\",\"i\" ) && "
22
+ clauses = search.split(' ').collect do |i|
23
+ %((regex(?name, "#{i}", "i") || regex(?alt, "#{i}", "i")))
25
24
  end
26
- ex = ex[0..ex.length - 4]
27
- ex += ') && ('
28
- search.split(' ').each do |i|
29
- ex += "regex(?name, \"#{i}\",\"i\" ) || "
30
- end
31
- ex = ex[0..ex.length - 4]
32
- ex += ") )"
33
-
25
+ ex = "(#{clauses.join(' && ')})"
34
26
  else
35
- ex = "regex(?name, \"#{search}\", \"i\")"
27
+ ex = %(regex(?name, "#{search}", "i"))
36
28
  end
37
-
38
- # The full text index matches on fields besides the term, so we filter to ensure the match is in the term.
39
- sparql = "SELECT DISTINCT ?s ?name ?par {
40
- ?s a skos:Concept; luc:term \"#{search}\";
41
- skos:inScheme <http://vocab.getty.edu/tgn/> ;
42
- gvp:prefLabelGVP [skosxl:literalForm ?name] ;
29
+ %(SELECT DISTINCT ?s ?name ?par {
30
+ ?s a skos:Concept; luc:term "#{search}";
31
+ skos:inScheme <http://vocab.getty.edu/ulan/> ;
32
+ gvp:prefLabelGVP [skosxl:literalForm ?name] ;
43
33
  gvp:parentString ?par .
44
- FILTER #{ex} .
45
- } ORDER BY ?name ASC(?par)"
46
- sparql
34
+ FILTER #{ex} .
35
+ } ORDER BY ?name ASC(?par)).gsub(/[\s\n]+/, " ")
47
36
  end
48
37
 
49
38
  def untaint(q)
@@ -15,25 +15,22 @@ module Qa::Authorities
15
15
  search = untaint(q)
16
16
  # if more than one term is supplied, check both preferred and alt labels
17
17
  if search.include?(' ')
18
- ex = "("
19
- search.split(' ').each do |i|
20
- ex += "regex(CONCAT(?name, ' ', ?alt), \"#{i}\",\"i\" ) && "
18
+ clauses = search.split(' ').collect do |i|
19
+ %((regex(?name, "#{i}", "i") || regex(?alt, "#{i}", "i")))
21
20
  end
22
- ex = ex[0..ex.length - 4]
23
- ex += ")"
21
+ ex = "(#{clauses.join(' && ')})"
24
22
  else
25
- ex = "regex(?name, \"#{search}\", \"i\")"
23
+ ex = %(regex(?name, "#{search}", "i"))
26
24
  end
27
25
  # The full text index matches on fields besides the term, so we filter to ensure the match is in the term.
28
- sparql = "SELECT DISTINCT ?s ?name ?bio {
29
- ?s a skos:Concept; luc:term \"#{search}\";
30
- skos:inScheme <http://vocab.getty.edu/ulan/> ;
31
- gvp:prefLabelGVP [skosxl:literalForm ?name] ;
32
- foaf:focus/gvp:biographyPreferred [schema:description ?bio] ;
33
- skos:altLabel ?alt .
34
- FILTER #{ex} .
35
- } ORDER BY ?name"
36
- sparql
26
+ %(SELECT DISTINCT ?s ?name ?bio {
27
+ ?s a skos:Concept; luc:term "#{search}";
28
+ skos:inScheme <http://vocab.getty.edu/ulan/> ;
29
+ gvp:prefLabelGVP [skosxl:literalForm ?name] ;
30
+ foaf:focus/gvp:biographyPreferred [schema:description ?bio] ;
31
+ skos:altLabel ?alt .
32
+ FILTER #{ex} .
33
+ } ORDER BY ?name).gsub(/[\s\n]+/, " ")
37
34
  end
38
35
 
39
36
  def untaint(q)
@@ -3,6 +3,7 @@ module Qa::Authorities
3
3
  extend ActiveSupport::Autoload
4
4
  autoload :GenericAuthority
5
5
  autoload :RdfHelper
6
+ autoload :AuthorityService
6
7
  autoload :SearchQuery
7
8
  autoload :FindTerm
8
9
  autoload :Config
@@ -0,0 +1,47 @@
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
4
+ module LinkedData
5
+ class AuthorityService
6
+ # Load or reload the linked data configuration files
7
+ def self.load_authorities
8
+ auth_cfg = {}
9
+ # load QA configured linked data authorities
10
+ Dir[File.join(Qa::Engine.root, 'config', 'authorities', 'linked_data', '*.json')].each do |fn|
11
+ auth = File.basename(fn, '.json').upcase.to_sym
12
+ json = File.read(File.expand_path(fn, __FILE__))
13
+ cfg = JSON.parse(json).deep_symbolize_keys
14
+ auth_cfg[auth] = cfg
15
+ end
16
+
17
+ # load app configured linked data authorities and overrides
18
+ Dir[Rails.root.join('config', 'authorities', 'linked_data', '*.json')].each do |fn|
19
+ auth = File.basename(fn, '.json').upcase.to_sym
20
+ json = File.read(File.expand_path(fn, __FILE__))
21
+ cfg = JSON.parse(json).deep_symbolize_keys
22
+ auth_cfg[auth] = cfg
23
+ end
24
+ Qa.config.linked_data_authority_configs = auth_cfg
25
+ end
26
+
27
+ # Get the list of names of the loaded authorities
28
+ # @return [Array<String>] all loaded authority configurations
29
+ def self.authority_configs
30
+ Qa.config.linked_data_authority_configs
31
+ end
32
+
33
+ # Get the configuration for an authority
34
+ # @param [String] name of the authority
35
+ # @return [Array<String>] configuration for the specified authority
36
+ def self.authority_config(authname)
37
+ authority_configs[authname]
38
+ end
39
+
40
+ # Get the list of names of the loaded authorities
41
+ # @return [Array<String>] names of the authority config files that are currently loaded
42
+ def self.authority_names
43
+ authority_configs.keys.sort
44
+ end
45
+ end
46
+ end
47
+ end
@@ -44,7 +44,7 @@ module Qa::Authorities
44
44
  # Return the full configuration for an authority
45
45
  # @return [String] the authority configuration
46
46
  def auth_config
47
- @authority_config ||= LINKED_DATA_AUTHORITIES_CONFIG[@authority_name]
47
+ @authority_config ||= Qa::Authorities::LinkedData::AuthorityService.authority_config(@authority_name)
48
48
  raise Qa::InvalidLinkedDataAuthority, "Unable to initialize linked data authority '#{@authority_name}'" if @authority_config.nil?
49
49
  @authority_config
50
50
  end
@@ -33,13 +33,14 @@ module Qa::Authorities
33
33
  # "http://schema.org/name":["Cornell University","Ithaca (N.Y.). Cornell University"],
34
34
  # "http://www.w3.org/2004/02/skos/core#altLabel":["Ithaca (N.Y.). Cornell University"],
35
35
  # "http://schema.org/sameAs":["http://id.loc.gov/authorities/names/n79021621","https://viaf.org/viaf/126293486"] } }
36
- def find(id, language: nil, replacements: {}, subauth: nil)
36
+ def find(id, language: nil, replacements: {}, subauth: nil, jsonld: false)
37
37
  raise Qa::InvalidLinkedDataAuthority, "Unable to initialize linked data term sub-authority #{subauth}" unless subauth.nil? || term_subauthority?(subauth)
38
38
  language ||= term_config.term_language
39
39
  url = term_config.term_url_with_replacements(id, subauth, replacements)
40
40
  Rails.logger.info "QA Linked Data term url: #{url}"
41
41
  graph = get_linked_data(url)
42
42
  return "{}" unless graph.size.positive?
43
+ return graph.dump(:jsonld, standard_prefixes: true) if jsonld
43
44
  parse_term_authority_response(id, graph, language)
44
45
  end
45
46
 
@@ -22,7 +22,13 @@ module Qa::Authorities
22
22
  @auth_config = Qa::Authorities::LinkedData::Config.new(auth_name)
23
23
  end
24
24
 
25
- include WebServiceBase
25
+ def reload_authorities
26
+ @authorities_service.load_authorities
27
+ end
28
+
29
+ def authorities_service
30
+ @authorities_service ||= Qa::Authorities::LinkedData::AuthorityService
31
+ end
26
32
 
27
33
  def search_service
28
34
  @search_service ||= Qa::Authorities::LinkedData::SearchQuery.new(search_config)
@@ -34,6 +40,7 @@ module Qa::Authorities
34
40
 
35
41
  delegate :search, to: :search_service
36
42
  delegate :find, to: :item_service
43
+ delegate :load_authorities, :authority_names, to: :authorities_service
37
44
 
38
45
  private
39
46
 
@@ -12,5 +12,23 @@ module Qa
12
12
  def disable_cors_headers
13
13
  @cors_headers_enabled = false
14
14
  end
15
+
16
+ # Provide a token that allows reloading of linked data authorities through the controller
17
+ # action '/reload/linked_data/authorities?auth_token=YOUR_AUTH_TOKEN_DEFINED_HERE' without
18
+ # requiring a restart of rails. By default, reloading through the browser is not allowed
19
+ # when the token is nil or blank. Change to your approved token string in
20
+ # config/initializers/qa.rb.
21
+ attr_writer :authorized_reload_token
22
+ def authorized_reload_token
23
+ @authorized_reload_token ||= nil
24
+ end
25
+
26
+ def valid_authority_reload_token?(token)
27
+ return false if token.blank? || authorized_reload_token.blank?
28
+ token == authorized_reload_token
29
+ end
30
+
31
+ # Hold linked data authority configs
32
+ attr_accessor :linked_data_authority_configs
15
33
  end
16
34
  end
@@ -1,3 +1,3 @@
1
1
  module Qa
2
- VERSION = "2.1.2".freeze
2
+ VERSION = "2.2.0".freeze
3
3
  end
@@ -61,6 +61,14 @@ describe Qa::LinkedDataTermsController, type: :controller do
61
61
  end
62
62
  end
63
63
 
64
+ describe '#check_uri_param' do
65
+ it 'returns 400 if the uri is missing' do
66
+ expect(Rails.logger).to receive(:warn).with("Required fetch param 'uri' is missing or empty")
67
+ get :fetch, params: { uri: '', vocab: 'OCLC_FAST' }
68
+ expect(response.code).to eq('400')
69
+ end
70
+ end
71
+
64
72
  describe '#init_authority' do
65
73
  context 'when the authority does not exist' do
66
74
  it 'returns 400' do
@@ -71,6 +79,18 @@ describe Qa::LinkedDataTermsController, type: :controller do
71
79
  end
72
80
  end
73
81
 
82
+ describe '#list' do
83
+ let(:expected_results) { ['Auth1', 'Auth2', 'Auth3'] }
84
+ before do
85
+ allow(Qa::Authorities::LinkedData::AuthorityService).to receive(:authority_names).and_return(expected_results)
86
+ end
87
+ it 'returns list of authorities' do
88
+ get :list
89
+ expect(response).to be_successful
90
+ expect(response.body).to eq expected_results.to_json
91
+ end
92
+ end
93
+
74
94
  describe '#search' do
75
95
  context 'producing internal server error' do
76
96
  context 'when server returns 500' do
@@ -324,9 +344,28 @@ describe Qa::LinkedDataTermsController, type: :controller do
324
344
  stub_request(:get, 'http://artemide.art.uniroma2.it:8081/agrovoc/rest/v1/data?uri=http://aims.fao.org/aos/agrovoc/c_9513')
325
345
  .to_return(status: 200, body: webmock_fixture('lod_agrovoc_term_found.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
326
346
  end
327
- it 'succeeds' do
347
+
348
+ it 'succeeds and defaults to json content type' do
328
349
  get :show, params: { id: 'c_9513', vocab: 'AGROVOC' }
329
350
  expect(response).to be_successful
351
+ expect(response.content_type).to eq 'application/json'
352
+ end
353
+
354
+ context 'and it was requested as json' do
355
+ it 'succeeds and returns term data as json content type' do
356
+ get :show, params: { id: 'c_9513', vocab: 'AGROVOC', format: 'json' }
357
+ expect(response).to be_successful
358
+ expect(response.content_type).to eq 'application/json'
359
+ end
360
+ end
361
+
362
+ context 'and it was requested as jsonld' do
363
+ it 'succeeds and returns term data as jsonld content type' do
364
+ get :show, params: { id: 'c_9513', vocab: 'AGROVOC', format: 'jsonld' }
365
+ expect(response).to be_successful
366
+ expect(response.content_type).to eq 'application/ld+json'
367
+ expect(JSON.parse(response.body).keys).to match_array ["@context", "@graph"]
368
+ end
330
369
  end
331
370
  end
332
371
  end
@@ -337,11 +376,150 @@ describe Qa::LinkedDataTermsController, type: :controller do
337
376
  stub_request(:get, 'http://id.loc.gov/authorities/subjects/sh85118553')
338
377
  .to_return(status: 200, body: webmock_fixture('lod_loc_term_found.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
339
378
  end
340
- it 'succeeds' do
379
+ it 'succeeds and defaults to json content type' do
341
380
  get :show, params: { id: 'sh85118553', vocab: 'LOC', subauthority: 'subjects' }
342
381
  expect(response).to be_successful
382
+ expect(response.content_type).to eq 'application/json'
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ describe '#fetch' do
389
+ context 'producing internal server error' do
390
+ context 'when server returns 500' do
391
+ before do
392
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369').to_return(status: 500)
393
+ end
394
+ it 'returns 500' do
395
+ expect(Rails.logger).to receive(:warn).with("Internal Server Error - Fetch term http://test.org/530369 unsuccessful for authority LOD_TERM_URI_PARAM_CONFIG")
396
+ get :fetch, params: { vocab: 'LOD_TERM_URI_PARAM_CONFIG', uri: 'http://test.org/530369' }
397
+ expect(response.code).to eq('500')
343
398
  end
344
399
  end
400
+
401
+ context 'when rdf format error' do
402
+ before do
403
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369').to_return(status: 200)
404
+ allow(RDF::Graph).to receive(:load).and_raise(RDF::FormatError)
405
+ end
406
+ it 'returns 500' do
407
+ msg = "RDF Format Error - Results from fetch term http://test.org/530369 for authority LOD_TERM_URI_PARAM_CONFIG was not identified as a valid RDF format. " \
408
+ "You may need to include the linkeddata gem."
409
+ expect(Rails.logger).to receive(:warn).with(msg)
410
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
411
+ expect(response.code).to eq('500')
412
+ end
413
+ end
414
+
415
+ context "when error isn't specifically handled" do
416
+ before do
417
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369').to_return(status: 501)
418
+ end
419
+ it 'returns 500' do
420
+ expect(Rails.logger).to receive(:warn).with("Internal Server Error - Fetch term http://test.org/530369 unsuccessful for authority LOD_TERM_URI_PARAM_CONFIG")
421
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
422
+ expect(response.code).to eq('500')
423
+ end
424
+ end
425
+ end
426
+
427
+ context 'when service unavailable' do
428
+ before do
429
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369').to_return(status: 503)
430
+ end
431
+ it 'returns 503' do
432
+ expect(Rails.logger).to receive(:warn).with("Service Unavailable - Fetch term http://test.org/530369 unsuccessful for authority LOD_TERM_URI_PARAM_CONFIG")
433
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
434
+ expect(response.code).to eq('503')
435
+ end
436
+ end
437
+
438
+ context 'when requested term is not found at the server' do
439
+ before do
440
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/FAKE_ID').to_return(status: 404, body: '', headers: {})
441
+ end
442
+ it 'returns 404' do
443
+ expect(Rails.logger).to receive(:warn).with('Term Not Found - Fetch term http://test.org/FAKE_ID unsuccessful for authority LOD_TERM_URI_PARAM_CONFIG')
444
+ get :fetch, params: { uri: 'http://test.org/FAKE_ID', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
445
+ expect(response.code).to eq('404')
446
+ end
447
+ end
448
+
449
+ context 'in LOD_TERM_URI_PARAM_CONFIG authority' do
450
+ context 'term found' do
451
+ before do
452
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369')
453
+ .to_return(status: 200, body: webmock_fixture('lod_oclc_term_found.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
454
+ end
455
+
456
+ it 'succeeds and defaults to json content type' do
457
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
458
+ expect(response).to be_successful
459
+ expect(response.content_type).to eq 'application/json'
460
+ end
461
+
462
+ context 'and it was requested as json' do
463
+ it 'succeeds and returns term data as json content type' do
464
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG', format: 'json' }
465
+ expect(response).to be_successful
466
+ expect(response.content_type).to eq 'application/json'
467
+ end
468
+ end
469
+
470
+ context 'and it was requested as jsonld' do
471
+ it 'succeeds and returns term data as jsonld content type' do
472
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG', format: 'jsonld' }
473
+ expect(response).to be_successful
474
+ expect(response.content_type).to eq 'application/ld+json'
475
+ expect(JSON.parse(response.body).keys).to match_array ["@context", "@graph"]
476
+ end
477
+ end
478
+ end
479
+
480
+ context 'when cors headers are enabled' do
481
+ before do
482
+ Qa.config.enable_cors_headers
483
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369')
484
+ .to_return(status: 200, body: webmock_fixture('lod_oclc_term_found.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
485
+ end
486
+ it 'Access-Control-Allow-Origin is *' do
487
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
488
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
489
+ end
490
+ end
491
+
492
+ context 'when cors headers are disabled' do
493
+ before do
494
+ Qa.config.disable_cors_headers
495
+ stub_request(:get, 'http://localhost/test_default/term?uri=http://test.org/530369')
496
+ .to_return(status: 200, body: webmock_fixture('lod_oclc_term_found.rdf.xml'), headers: { 'Content-Type' => 'application/rdf+xml' })
497
+ end
498
+ it 'Access-Control-Allow-Origin is not present' do
499
+ get :fetch, params: { uri: 'http://test.org/530369', vocab: 'LOD_TERM_URI_PARAM_CONFIG' }
500
+ expect(response.headers.key?('Access-Control-Allow-Origin')).to be false
501
+ end
502
+ end
503
+ end
504
+ end
505
+
506
+ describe '#reload' do
507
+ before do
508
+ Qa.config.authorized_reload_token = 'A_TOKEN'
509
+ end
510
+
511
+ context 'when token does not match' do
512
+ it 'returns 401' do
513
+ get :reload, params: { auth_token: 'BAD_TOKEN' }
514
+ expect(response.code).to eq('401')
515
+ end
516
+ end
517
+
518
+ context 'when token does match' do
519
+ it 'returns 200' do
520
+ get :reload, params: { auth_token: 'A_TOKEN' }
521
+ expect(response.code).to eq('200')
522
+ end
345
523
  end
346
524
  end
347
525
  end