qa 2.1.2 → 2.2.0

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