bento_search 1.5.0 → 2.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 +27 -24
- data/Rakefile +30 -11
- data/app/assets/javascripts/bento_search/ajax_load.js +54 -22
- data/app/controllers/bento_search/search_controller.rb +31 -30
- data/app/helpers/bento_search_helper.rb +72 -74
- data/app/models/bento_search/concurrent_searcher.rb +136 -0
- data/app/models/bento_search/result_item.rb +15 -12
- data/app/models/bento_search/results/serialization.rb +22 -13
- data/app/models/bento_search/search_engine.rb +170 -140
- data/app/search_engines/bento_search/doaj_articles_engine.rb +20 -20
- data/app/search_engines/bento_search/ebsco_host_engine.rb +3 -3
- data/app/search_engines/bento_search/eds_engine.rb +326 -206
- data/app/search_engines/bento_search/google_books_engine.rb +2 -2
- data/app/search_engines/bento_search/scopus_engine.rb +87 -87
- data/app/search_engines/bento_search/summon_engine.rb +1 -1
- data/app/views/bento_search/_ajax_loading.html.erb +17 -0
- data/app/views/bento_search/_item_title.html.erb +2 -4
- data/app/views/bento_search/_link.html.erb +3 -3
- data/lib/bento_search.rb +24 -9
- data/lib/bento_search/engine.rb +2 -0
- data/lib/bento_search/version.rb +1 -1
- data/lib/generators/bento_search/install/ajax_load_js_generator.rb +15 -0
- data/test/decorator/standard_decorator_test.rb +30 -30
- data/test/dummy/app/assets/config/manifest.js +4 -0
- data/test/dummy/config/application.rb +7 -0
- data/test/dummy/config/boot.rb +4 -9
- data/test/dummy/config/environments/development.rb +2 -0
- data/test/dummy/config/environments/production.rb +7 -1
- data/test/dummy/config/environments/test.rb +10 -3
- data/test/functional/bento_search/search_controller_test.rb +68 -58
- data/test/helper/bento_search_helper_test.rb +103 -103
- data/test/search_engines/doaj_articles_engine_test.rb +9 -9
- data/test/search_engines/eds_engine_test.rb +91 -59
- data/test/search_engines/google_site_search_test.rb +48 -48
- data/test/search_engines/scopus_engine_test.rb +51 -51
- data/test/search_engines/search_engine_base_test.rb +108 -86
- data/test/search_engines/search_engine_test.rb +68 -56
- data/test/support/atom.xsd.xml +3 -3
- data/test/support/xml.xsd +117 -0
- data/test/test_helper.rb +23 -12
- data/test/unit/concurrent_searcher_test.rb +75 -0
- data/test/unit/pagination_test.rb +12 -12
- data/test/vcr_cassettes/eds/FullText_CustomLink.yml +198 -0
- data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1036 -1729
- data/test/vcr_cassettes/eds/catalog_ebook_query.yml +218 -0
- data/test/vcr_cassettes/eds/catalog_query.yml +255 -0
- data/test/vcr_cassettes/eds/get_auth_token.yml +11 -44
- data/test/vcr_cassettes/eds/get_auth_token_failure.yml +10 -7
- data/test/vcr_cassettes/eds/get_with_auth.yml +144 -153
- data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +167 -223
- data/test/view/atom_results_test.rb +94 -94
- metadata +36 -46
- data/app/assets/javascripts/bento_search.js +0 -3
- data/app/item_decorators/bento_search/ebscohost/conditional_openurl_main_link.rb +0 -36
- data/app/item_decorators/bento_search/only_premade_openurl.rb +0 -20
- data/app/item_decorators/bento_search/openurl_add_other_link.rb +0 -39
- data/app/item_decorators/bento_search/openurl_main_link.rb +0 -34
- data/app/models/bento_search/multi_searcher.rb +0 -131
- data/test/dummy/config/initializers/secret_token.rb +0 -8
- data/test/unit/multi_searcher_test.rb +0 -49
@@ -4,10 +4,10 @@ require 'http_client_patch/include_client'
|
|
4
4
|
require 'json'
|
5
5
|
|
6
6
|
module BentoSearch
|
7
|
-
# DOAJ Articles search.
|
7
|
+
# DOAJ Articles search.
|
8
8
|
# https://doaj.org/api/v1/docs
|
9
9
|
#
|
10
|
-
# Phrase searches with double quotes are respected.
|
10
|
+
# Phrase searches with double quotes are respected.
|
11
11
|
#
|
12
12
|
# Supports #get by unique_id feature
|
13
13
|
#
|
@@ -16,7 +16,7 @@ module BentoSearch
|
|
16
16
|
include ActionView::Helpers::SanitizeHelper
|
17
17
|
|
18
18
|
|
19
|
-
class_attribute :http_timeout
|
19
|
+
class_attribute :http_timeout, instance_writer: false
|
20
20
|
self.http_timeout = 10
|
21
21
|
|
22
22
|
extend HTTPClientPatch::IncludeClient
|
@@ -36,7 +36,7 @@ module BentoSearch
|
|
36
36
|
Rails.logger.debug("DoajEngine: requesting #{query_url}")
|
37
37
|
response = http_client.get( query_url )
|
38
38
|
json = JSON.parse(response.body)
|
39
|
-
rescue
|
39
|
+
rescue BentoSearch::RubyTimeoutClass, HTTPClient::TimeoutError,
|
40
40
|
HTTPClient::ConfigurationError, HTTPClient::BadResponseError,
|
41
41
|
JSON::ParserError => e
|
42
42
|
results.error ||= {}
|
@@ -77,7 +77,7 @@ module BentoSearch
|
|
77
77
|
def args_to_search_url(arguments)
|
78
78
|
query = if arguments[:query].kind_of?(Hash)
|
79
79
|
# multi-field query
|
80
|
-
arguments[:query].collect {|field,
|
80
|
+
arguments[:query].collect {|field, query_value| fielded_query(query_value, field)}.join(" ")
|
81
81
|
else
|
82
82
|
fielded_query(arguments[:query], arguments[:search_field])
|
83
83
|
end
|
@@ -85,7 +85,7 @@ module BentoSearch
|
|
85
85
|
# We need to escape this for going in a PATH component,
|
86
86
|
# not a query. So space can't be "+", it needs to be "%20",
|
87
87
|
# and indeed DOAJ API does not like "+".
|
88
|
-
#
|
88
|
+
#
|
89
89
|
# But neither CGI.escape nor URI.escape does quite
|
90
90
|
# the right kind of escaping, seems to work out
|
91
91
|
# if we do CGI.escape but then replace '+'
|
@@ -98,7 +98,7 @@ module BentoSearch
|
|
98
98
|
if arguments[:per_page]
|
99
99
|
query_args["pageSize"] = arguments[:per_page]
|
100
100
|
end
|
101
|
-
|
101
|
+
|
102
102
|
if arguments[:page]
|
103
103
|
query_args["page"] = arguments[:page]
|
104
104
|
end
|
@@ -115,14 +115,14 @@ module BentoSearch
|
|
115
115
|
return url
|
116
116
|
end
|
117
117
|
|
118
|
-
# Prepares a DOAJ API (elastic search) query component for
|
118
|
+
# Prepares a DOAJ API (elastic search) query component for
|
119
119
|
# given textual query in a given field (or default non-fielded search)
|
120
120
|
#
|
121
121
|
# Separates query string into tokens (bare words and phrases),
|
122
122
|
# so they can each be made mandatory for ElasticSearch. Default
|
123
123
|
# DOAJ API makes them all optional, with a very low mm, which
|
124
124
|
# leads to low-precision odd looking results for standard use
|
125
|
-
# cases.
|
125
|
+
# cases.
|
126
126
|
#
|
127
127
|
# Escapes all remaining special characters as literals (not including
|
128
128
|
# double quotes which can be used for phrases, which are respected. )
|
@@ -133,7 +133,7 @@ module BentoSearch
|
|
133
133
|
#
|
134
134
|
# The "+" prefixed before field-name is to make sure all separate
|
135
135
|
# fields are also mandatory when doing multi-field searches. It should
|
136
|
-
# make no difference for a single-field search.
|
136
|
+
# make no difference for a single-field search.
|
137
137
|
def fielded_query(query, field = nil)
|
138
138
|
if field.present?
|
139
139
|
"+#{field}:(#{prepare_mandatory_terms(query)})"
|
@@ -143,12 +143,12 @@ module BentoSearch
|
|
143
143
|
end
|
144
144
|
|
145
145
|
# Takes a query string, prepares an ElasticSearch query
|
146
|
-
# doing what we want:
|
146
|
+
# doing what we want:
|
147
147
|
# * tokenizes into bare words and double-quoted phrases
|
148
148
|
# * Escapes other punctuation to be literal not ElasticSearch operator.
|
149
149
|
# (Does NOT do URI escaping)
|
150
|
-
# * Makes each token mandatory with an ElasticSearch "+" operator prefixed.
|
151
|
-
def prepare_mandatory_terms(query)
|
150
|
+
# * Makes each token mandatory with an ElasticSearch "+" operator prefixed.
|
151
|
+
def prepare_mandatory_terms(query)
|
152
152
|
# use string split with regex to too-cleverly split into space
|
153
153
|
# seperated terms and phrases, keeping phrases as unit.
|
154
154
|
terms = query.split %r{[[:space:]]+|("[^"]+")}
|
@@ -174,13 +174,13 @@ module BentoSearch
|
|
174
174
|
|
175
175
|
item.start_page = bibjson["start_page"]
|
176
176
|
item.end_page = bibjson["end_page"]
|
177
|
-
|
177
|
+
|
178
178
|
item.year = bibjson["year"]
|
179
179
|
if (year = bibjson["year"].to_i) && (month = bibjson["month"].to_i)
|
180
180
|
if year != 0 && month != 0
|
181
181
|
item.publication_date = Date.new(bibjson["year"].to_i, bibjson["month"].to_i)
|
182
182
|
end
|
183
|
-
end
|
183
|
+
end
|
184
184
|
|
185
185
|
item.abstract = sanitize(bibjson["abstract"]) if bibjson.has_key?("abstract")
|
186
186
|
|
@@ -222,9 +222,9 @@ module BentoSearch
|
|
222
222
|
# punctuation that needs to be escaped and how to escape (backslash)
|
223
223
|
# for ES documented here: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
|
224
224
|
#
|
225
|
-
# We do not escape double quotes, want to allow them for phrases.
|
225
|
+
# We do not escape double quotes, want to allow them for phrases.
|
226
226
|
#
|
227
|
-
# This method does NOT return URI-escaped, it returns literal, escaped for ES.
|
227
|
+
# This method does NOT return URI-escaped, it returns literal, escaped for ES.
|
228
228
|
def escape_query(q)
|
229
229
|
q.gsub(/([\+\-\=\&\|\>\<\!\(\)\{\}\[\]\^\~\*\?\:\\\/])/) {|m| "\\#{$1}"}
|
230
230
|
end
|
@@ -242,7 +242,7 @@ module BentoSearch
|
|
242
242
|
{ nil => {:semantic => :general},
|
243
243
|
"bibjson.title" => {:semantic => :title},
|
244
244
|
# Using 'exact' seems to produce much better results for
|
245
|
-
# author, don't entirely understand what's up.
|
245
|
+
# author, don't entirely understand what's up.
|
246
246
|
"bibjson.author.name" => {:semantic => :author},
|
247
247
|
"publisher" => {:semantic => :publisher},
|
248
248
|
"bibjson.subject.term" => {:semantic => :subject},
|
@@ -263,7 +263,7 @@ module BentoSearch
|
|
263
263
|
|
264
264
|
def sort_definitions
|
265
265
|
# Don't believe DOAJ supports sorting by author
|
266
|
-
{
|
266
|
+
{
|
267
267
|
"relevance" => {:implementation => nil}, # default
|
268
268
|
"title" => {:implementation => "title:asc"},
|
269
269
|
# We don't quite have publication date sorting, but we'll use
|
@@ -276,4 +276,4 @@ module BentoSearch
|
|
276
276
|
end
|
277
277
|
|
278
278
|
end
|
279
|
-
end
|
279
|
+
end
|
@@ -131,14 +131,14 @@ class BentoSearch::EbscoHostEngine
|
|
131
131
|
url = query_url(args)
|
132
132
|
|
133
133
|
Rails.logger.debug("EbscoHostEngine Search for: #{url}")
|
134
|
-
|
134
|
+
|
135
135
|
results = BentoSearch::Results.new
|
136
136
|
xml, response, exception = nil, nil, nil
|
137
137
|
|
138
138
|
begin
|
139
139
|
response = http_client.get(url)
|
140
140
|
xml = Nokogiri::XML(response.body)
|
141
|
-
rescue
|
141
|
+
rescue BentoSearch::RubyTimeoutClass, HTTPClient::ConfigurationError, HTTPClient::BadResponseError, Nokogiri::SyntaxError => e
|
142
142
|
exception = e
|
143
143
|
end
|
144
144
|
# error handle
|
@@ -361,7 +361,7 @@ class BentoSearch::EbscoHostEngine
|
|
361
361
|
query = if args[:query].kind_of?(Hash)
|
362
362
|
# multi-field query
|
363
363
|
args[:query].collect {|field, query| fielded_query(query, field)}.join(" AND ")
|
364
|
-
else
|
364
|
+
else
|
365
365
|
fielded_query(args[:query], args[:search_field])
|
366
366
|
end
|
367
367
|
|
@@ -7,145 +7,144 @@ require 'http_client_patch/include_client'
|
|
7
7
|
|
8
8
|
|
9
9
|
#
|
10
|
-
# For EBSCO Discovery Service. You will need a license to use.
|
10
|
+
# For EBSCO Discovery Service. You will need a license to use.
|
11
11
|
#
|
12
12
|
# == Required Configuration
|
13
13
|
#
|
14
|
-
# user_id, password: As given be EBSCO for access to EDS API (may be an admin account in ebscoadmin? Not sure).
|
14
|
+
# user_id, password: As given be EBSCO for access to EDS API (may be an admin account in ebscoadmin? Not sure).
|
15
15
|
# profile: As given by EBSCO, might be "edsapi"?
|
16
16
|
#
|
17
17
|
# == Highlighting
|
18
18
|
#
|
19
|
-
# EDS has a query-in-context highlighting feature. It is used by defualt, set
|
20
|
-
# config 'highlighting' to false to disable.
|
19
|
+
# EDS has a query-in-context highlighting feature. It is used by defualt, set
|
20
|
+
# config 'highlighting' to false to disable.
|
21
21
|
# If turned on, you may get <b class="bento_search_highlight"> tags
|
22
|
-
# in title and abstract output if it's on, marked html_safe.
|
22
|
+
# in title and abstract output if it's on, marked html_safe.
|
23
23
|
#
|
24
24
|
#
|
25
25
|
# == Linking
|
26
26
|
#
|
27
27
|
# The link to record in EBSCO interface delivered as "PLink" will be listed
|
28
|
-
# as record main link.
|
28
|
+
# as record main link. If the record includes a node at `./FullText/Links/Link/Type[text() = 'pdflink']`,
|
29
|
+
# the `plink` will be marked as fulltext. (There may be other cases of fulltext, but
|
30
|
+
# this seems to be all EDS API tells us.)
|
29
31
|
#
|
30
32
|
# Any links listed under <CustomLinks> will be listed as other_links, using
|
31
|
-
# configured name provided by EBSCO for CustomLink.
|
33
|
+
# configured name provided by EBSCO for CustomLink. Same with links listed
|
34
|
+
# as `<Item><Group>URL</Group>`.
|
32
35
|
#
|
33
|
-
#
|
34
|
-
# ourselves. However, in our testing, the first/only CustomLink was an
|
35
|
-
# an OpenURL. If configuration.assume_first_custom_link_openurl is
|
36
|
-
# true (as is default), it will be used to create an OpenURL link. However, in
|
37
|
-
# our testing, many records don't have this at all. **Note** You want
|
38
|
-
# to configure your profile so OpenURLs are ALWAYS included for all records, not
|
39
|
-
# just records with no EBSCO fulltext, to ensure bento_search can get the
|
40
|
-
# openurl. http://support.ebsco.com/knowledge_base/detail.php?id=1111 (May
|
41
|
-
# have to ask EBSCO support for help, it's confusing!).
|
36
|
+
# As always, you can customize links and other_links with Item Decorators.
|
42
37
|
#
|
43
|
-
#
|
44
|
-
# it's configured name or label, not assume first one is it.
|
38
|
+
# == Custom Data
|
45
39
|
#
|
46
|
-
#
|
40
|
+
# If present, there is a custom_data[:holdings] value, an array of
|
41
|
+
# BentoSearch::EdsEngine::Holding objects, each of which has a #location
|
42
|
+
# and #call_number. There will usually (always?) be at most 1 item in the
|
43
|
+
# array, as far as we can tell from how EDS works.
|
47
44
|
#
|
48
45
|
# == Technical Notes and Difficulties
|
49
46
|
#
|
50
|
-
# This API is
|
51
|
-
#
|
52
|
-
#
|
47
|
+
# This API is pretty difficult to work with, and the response has many
|
48
|
+
# idiosyncratic undocumented parts. We think we are currently
|
49
|
+
# getting fairly complete citation detail out, at least for articles, but may be missing
|
50
|
+
# some on weird edge cases, books/book chapters, etc)
|
53
51
|
#
|
54
52
|
# Auth issues may make this slow -- you need to spend a (not too speedy) HTTP
|
55
53
|
# request making a session for every new end-user -- as we have no way to keep
|
56
|
-
# track of end-users, we do it on every request in this implementation.
|
54
|
+
# track of end-users, we do it on every request in this implementation.
|
57
55
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
# and
|
64
|
-
# made from individual elements.
|
65
|
-
#
|
66
|
-
# EBSCO says they plan to improve some of these issues in a September 2012 release.
|
56
|
+
# An older version of the EDS API returned much less info, and we tried
|
57
|
+
# to scrape out what we could anyway. Much of this logic is still there
|
58
|
+
# as backup. In the older version, not enough info was there for an
|
59
|
+
# OpenURL link, `configuration.assume_first_custom_link_openurl` was true
|
60
|
+
# by default, and used to create an OpenURL link. It now defaults to false,
|
61
|
+
# and should no longer be neccessary.
|
67
62
|
#
|
68
63
|
# Title and abstract data seems to be HTML with tags and character entities and
|
69
|
-
# escaped special chars. We're trusting it and passing it on as html_safe.
|
64
|
+
# escaped special chars. We're trusting it and passing it on as html_safe.
|
70
65
|
#
|
71
66
|
# Paging can only happen on even pages, with 'page' rather than 'start'. But
|
72
|
-
# you can pass in 'start' to bento_search, it'll be converted to closest page.
|
67
|
+
# you can pass in 'start' to bento_search, it'll be converted to closest page.
|
73
68
|
#
|
74
69
|
# == Authenticated Users
|
75
70
|
#
|
76
|
-
# EDS allows searches by unauthenticated users, but the results come back with
|
71
|
+
# EDS allows searches by unauthenticated users, but the results come back with
|
77
72
|
# weird blank hits. In such a case, the BentoSearch adapter will return
|
78
73
|
# records with virtually no metadata, but a title e
|
79
74
|
# (I18n at bento_search.eds.record_not_available ). Also no abstracts
|
80
|
-
# are available from unauth search.
|
75
|
+
# are available from unauth search.
|
81
76
|
#
|
82
77
|
# By default the engine will search as 'guest' unauth user. But config
|
83
78
|
# 'auth' key to true to force all searches to auth (if you are protecting your
|
84
|
-
# app) or pass :auth => true as param into #search method.
|
79
|
+
# app) or pass :auth => true as param into #search method.
|
85
80
|
#
|
86
81
|
# == Source Types
|
87
82
|
# # What the EBSCO 'source types' mean: http://suprpot.ebsco.com/knowledge_base/detail.php?id=5382
|
88
83
|
#
|
89
|
-
# But "Dissertations" not "Dissertations/Theses". "Music Scores" not "Music Score".
|
84
|
+
# But "Dissertations" not "Dissertations/Theses". "Music Scores" not "Music Score".
|
90
85
|
|
91
86
|
#
|
92
87
|
# == EDS docs:
|
93
|
-
#
|
94
|
-
# * Console App to demo requests:
|
88
|
+
#
|
89
|
+
# * Console App to demo requests: <
|
95
90
|
# * EDS Wiki: http://edswiki.ebscohost.com/EDS_API_Documentation
|
96
91
|
# * You'll need to request an account to the EDS wiki, see: http://support.ebsco.com/knowledge_base/detail.php?id=5990
|
97
|
-
#
|
92
|
+
#
|
98
93
|
|
99
94
|
class BentoSearch::EdsEngine
|
100
95
|
include BentoSearch::SearchEngine
|
101
|
-
|
96
|
+
|
102
97
|
# Can't change http timeout in config, because we keep an http
|
103
|
-
# client at class-wide level, and config is not class-wide.
|
104
|
-
#
|
98
|
+
# client at class-wide level, and config is not class-wide.
|
99
|
+
# We used to keep in constant, but that's not good for custom setting,
|
100
|
+
# we now use class_attribute, but in a weird backwards-compat way for
|
101
|
+
# anyone who might be using the constant.
|
105
102
|
HttpTimeout = 4
|
106
|
-
|
103
|
+
|
104
|
+
class_attribute :http_timeout, instance_writer: false
|
105
|
+
def self.http_timeout
|
106
|
+
defined?(@http_timeout) ? @http_timeout : HttpTimeout
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
extend HTTPClientPatch::IncludeClient
|
107
111
|
include_http_client do |client|
|
108
|
-
client.connect_timeout = client.send_timeout = client.receive_timeout =
|
112
|
+
client.connect_timeout = client.send_timeout = client.receive_timeout = http_timeout
|
109
113
|
end
|
110
|
-
|
114
|
+
|
111
115
|
AuthHeader = "x-authenticationToken"
|
112
116
|
SessionTokenHeader = "x-sessionToken"
|
113
117
|
|
114
118
|
@@remembered_auth = nil
|
115
119
|
@@remembered_auth_lock = Mutex.new
|
116
120
|
# Class variable to save current known good auth
|
117
|
-
# uses a mutex to be threadsafe. sigh.
|
121
|
+
# uses a mutex to be threadsafe. sigh.
|
118
122
|
def self.remembered_auth
|
119
|
-
@@remembered_auth_lock.synchronize do
|
123
|
+
@@remembered_auth_lock.synchronize do
|
120
124
|
@@remembered_auth
|
121
125
|
end
|
122
126
|
end
|
123
|
-
# Set class variable with current known good auth.
|
124
|
-
# uses a mutex to be threadsafe.
|
127
|
+
# Set class variable with current known good auth.
|
128
|
+
# uses a mutex to be threadsafe.
|
125
129
|
def self.remembered_auth=(token)
|
126
130
|
@@remembered_auth_lock.synchronize do
|
127
131
|
@@remembered_auth = token
|
128
132
|
end
|
129
133
|
end
|
130
|
-
|
134
|
+
|
131
135
|
# an object that includes some Rails helper modules for
|
132
|
-
# text handling.
|
136
|
+
# text handling.
|
133
137
|
def helper
|
134
|
-
|
135
|
-
@helper = Object.new
|
136
|
-
@helper.extend ActionView::Helpers::TextHelper # for truncate
|
137
|
-
@helper.extend ActionView::Helpers::OutputSafetyHelper # for safe_join
|
138
|
-
end
|
139
|
-
return @helper
|
138
|
+
@helper ||= Helper.new
|
140
139
|
end
|
141
|
-
|
142
|
-
|
140
|
+
|
141
|
+
|
143
142
|
def self.required_configuration
|
144
143
|
%w{user_id password profile}
|
145
144
|
end
|
146
|
-
|
145
|
+
|
147
146
|
# From config or args, args over-ride config
|
148
|
-
def authenticated_end_user?(args)
|
147
|
+
def authenticated_end_user?(args)
|
149
148
|
config = configuration.auth ? true : false
|
150
149
|
arg = args[:auth]
|
151
150
|
if ! arg.nil?
|
@@ -156,164 +155,269 @@ class BentoSearch::EdsEngine
|
|
156
155
|
false
|
157
156
|
end
|
158
157
|
end
|
159
|
-
|
158
|
+
|
160
159
|
def construct_search_url(args)
|
161
160
|
query = "AND,"
|
162
161
|
if args[:search_field]
|
163
162
|
query += "#{args[:search_field]}:"
|
164
163
|
end
|
165
164
|
# Can't have any commas in query, it turns out, although
|
166
|
-
# this is not documented.
|
165
|
+
# this is not documented.
|
167
166
|
query += args[:query].gsub(",", " ")
|
168
|
-
|
167
|
+
|
169
168
|
url = "#{configuration.base_url}search?view=detailed&query=#{CGI.escape query}"
|
170
|
-
|
169
|
+
|
171
170
|
url += "&searchmode=#{CGI.escape configuration.search_mode}"
|
172
|
-
|
171
|
+
|
173
172
|
url += "&highlight=#{configuration.highlighting ? 'y' : 'n' }"
|
174
|
-
|
173
|
+
|
175
174
|
if args[:per_page]
|
176
175
|
url += "&resultsperpage=#{args[:per_page]}"
|
177
176
|
end
|
178
177
|
if args[:page]
|
179
178
|
url += "&pagenumber=#{args[:page]}"
|
180
179
|
end
|
181
|
-
|
180
|
+
|
182
181
|
if args[:sort]
|
183
182
|
if (defn = self.sort_definitions[args[:sort]]) &&
|
184
183
|
(value = defn[:implementation] )
|
185
184
|
url += "&sort=#{CGI.escape value}"
|
186
185
|
end
|
187
186
|
end
|
188
|
-
|
187
|
+
|
189
188
|
if configuration.only_source_types.present?
|
190
189
|
# facetfilter=1,SourceType:Research Starters,SourceType:Books
|
191
190
|
url += "&facetfilter=" + CGI.escape("1," + configuration.only_source_types.collect {|t| "SourceType:#{t}"}.join(","))
|
192
191
|
end
|
193
|
-
|
194
|
-
|
192
|
+
|
193
|
+
|
195
194
|
return url
|
196
195
|
end
|
197
|
-
|
198
|
-
|
199
|
-
|
196
|
+
|
197
|
+
|
198
|
+
|
200
199
|
def search_implementation(args)
|
201
200
|
results = BentoSearch::Results.new
|
202
|
-
|
201
|
+
|
203
202
|
end_user_auth = authenticated_end_user? args
|
204
|
-
|
203
|
+
|
205
204
|
begin
|
206
205
|
with_session(end_user_auth) do |session_token|
|
207
|
-
|
206
|
+
|
208
207
|
url = construct_search_url(args)
|
209
|
-
|
210
|
-
|
211
|
-
|
208
|
+
|
212
209
|
response = get_with_auth(url, session_token)
|
213
|
-
|
210
|
+
|
214
211
|
results = BentoSearch::Results.new
|
215
|
-
|
216
|
-
if (hits_node = at_xpath_text(response, "./SearchResponseMessageGet/SearchResult/Statistics/TotalHits"))
|
212
|
+
|
213
|
+
if (hits_node = at_xpath_text(response, "./SearchResponseMessageGet/SearchResult/Statistics/TotalHits"))
|
217
214
|
results.total_items = hits_node.to_i
|
218
215
|
end
|
219
|
-
|
216
|
+
|
220
217
|
response.xpath("./SearchResponseMessageGet/SearchResult/Data/Records/Record").each do |record_xml|
|
221
218
|
item = BentoSearch::ResultItem.new
|
222
|
-
|
219
|
+
|
223
220
|
item.title = prepare_eds_payload( element_by_group(record_xml, "Ti"), true )
|
224
|
-
|
221
|
+
|
225
222
|
# To get a unique id, we need to pull out db code and accession number
|
226
|
-
# and combine em with colon, accession number is not unique by itself.
|
223
|
+
# and combine em with colon, accession number is not unique by itself.
|
227
224
|
db = record_xml.at_xpath("./Header/DbId").try(:text)
|
228
225
|
accession = record_xml.at_xpath("./Header/An").try(:text)
|
229
226
|
if db && accession
|
230
227
|
item.unique_id = "#{db}:#{accession}"
|
231
228
|
end
|
232
|
-
|
233
|
-
|
229
|
+
|
230
|
+
|
234
231
|
if item.title.nil? && ! end_user_auth
|
235
232
|
item.title = I18n.translate("bento_search.eds.record_not_available")
|
236
233
|
end
|
237
|
-
|
234
|
+
|
238
235
|
item.abstract = prepare_eds_payload( element_by_group(record_xml, "Ab"), true )
|
239
236
|
|
240
|
-
#
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
# only SOMETIMES does it have XML tags, other times it's straight text.
|
246
|
-
# ARGH.
|
247
|
-
author_xml = Nokogiri::XML::fragment(author_mess)
|
248
|
-
searchLinks = author_xml.xpath(".//searchLink")
|
249
|
-
if searchLinks.size > 0
|
250
|
-
author_xml.xpath(".//searchLink").each do |author_node|
|
251
|
-
item.authors << BentoSearch::Author.new(:display => author_node.text)
|
237
|
+
# Much better way to get authors out of EDS response now...
|
238
|
+
author_full_names = record_xml.xpath("./RecordInfo/BibRecord/BibRelationships/HasContributorRelationships/HasContributor/PersonEntity/Name/NameFull")
|
239
|
+
author_full_names.each do |name_full_xml|
|
240
|
+
if name_full_xml && (text = name_full_xml.text).present?
|
241
|
+
item.authors << BentoSearch::Author.new(:display => text)
|
252
242
|
end
|
253
|
-
else
|
254
|
-
item.authors << BentoSearch::Author.new(:display => author_xml.text)
|
255
243
|
end
|
256
|
-
|
257
|
-
|
244
|
+
|
245
|
+
if item.authors.blank?
|
246
|
+
# Believe it or not, the authors are encoded as an escaped
|
247
|
+
# XML-ish payload, that we need to parse again and get the
|
248
|
+
# actual authors out of. WTF. Thanks for handling fragments
|
249
|
+
# nokogiri.
|
250
|
+
author_mess = element_by_group(record_xml, "Au")
|
251
|
+
# only SOMETIMES does it have XML tags, other times it's straight text.
|
252
|
+
# ARGH.
|
253
|
+
author_xml = Nokogiri::XML::fragment(author_mess)
|
254
|
+
searchLinks = author_xml.xpath(".//searchLink")
|
255
|
+
if searchLinks.size > 0
|
256
|
+
author_xml.xpath(".//searchLink").each do |author_node|
|
257
|
+
item.authors << BentoSearch::Author.new(:display => author_node.text)
|
258
|
+
end
|
259
|
+
else
|
260
|
+
item.authors << BentoSearch::Author.new(:display => author_xml.text)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
258
264
|
# PLink is main inward facing EBSCO link, put it as
|
259
|
-
# main link.
|
265
|
+
# main link.
|
260
266
|
if direct_link = record_xml.at_xpath("./PLink")
|
261
|
-
|
267
|
+
item.link = direct_link.text
|
268
|
+
|
269
|
+
if record_xml.at_xpath("./FullText/Links/Link/Type[text() = 'pdflink']")
|
270
|
+
item.link_is_fulltext = true
|
271
|
+
end
|
262
272
|
end
|
263
|
-
|
273
|
+
|
274
|
+
|
264
275
|
# Other links may be found in CustomLinks, it seems like usually
|
265
276
|
# there will be at least one, hopefully the first one is the OpenURL?
|
266
|
-
|
277
|
+
#byebug if configuration.id == "articles"
|
278
|
+
record_xml.xpath("./CustomLinks/CustomLink|./FullText/CustomLinks/CustomLink").each do |custom_link|
|
279
|
+
# If it's in FullText section, give it a rel=alternate
|
280
|
+
# to indicate it's fulltext
|
281
|
+
rel = (custom_link.parent.parent.name.downcase == "fulltext") ? "alternate" : nil
|
282
|
+
|
267
283
|
item.other_links << BentoSearch::Link.new(
|
268
284
|
:url => custom_link.at_xpath("./Url").text,
|
269
|
-
:
|
285
|
+
:rel => rel,
|
286
|
+
:label => custom_link.at_xpath("./Text").try(:text).presence || custom_link.at_xpath("./Name").try(:text).presence || "Link"
|
270
287
|
)
|
271
288
|
end
|
272
|
-
|
289
|
+
|
290
|
+
# More other links in 'URL' Item, in unpredictable format sometimes being
|
291
|
+
# embedded XML. Really EBSCO?
|
292
|
+
record_xml.xpath("./Items/Item[child::Group[text()='URL']]").each do |url_item|
|
293
|
+
data_element = url_item.at_xpath("./Data")
|
294
|
+
next unless data_element
|
295
|
+
|
296
|
+
# SOMETIMES the url and label are in an embedded escaped XML element...
|
297
|
+
if data_element.text.strip.start_with?("<link")
|
298
|
+
# Ugh, once unescpaed it has bare '&' in URL queries sometimes, which
|
299
|
+
# is not actually legal XML anymore, but Nokogiri::HTML parser will
|
300
|
+
# let us get away with it, but then doesn't put the actual text
|
301
|
+
# inside the 'link' item, but inside the <link> tag since it knows
|
302
|
+
# an HTML link tag has no content. Really EDS.
|
303
|
+
node = Nokogiri::HTML::fragment(data_element.text)
|
304
|
+
next unless link = node.at_xpath("./link")
|
305
|
+
next unless link["linkterm"].presence || link["linkTerm"].presence
|
306
|
+
|
307
|
+
item.other_links << BentoSearch::Link.new(
|
308
|
+
:url => link["linkterm"] || link["linkTerm"],
|
309
|
+
:label => helper.strip_tags(data_element.text).presence || "Link"
|
310
|
+
)
|
311
|
+
else
|
312
|
+
# it's just a straight URL in data element, with only label we've
|
313
|
+
# got in <label> element.
|
314
|
+
next unless data_element.text.strip.present?
|
315
|
+
|
316
|
+
label_element = url_item.at_xpath("./Label")
|
317
|
+
label = label_element.try(:text).try { |s| helper.strip_tags(s) }.presence || "Link"
|
318
|
+
|
319
|
+
item.other_links << BentoSearch::Link.new(
|
320
|
+
:url => data_element.text,
|
321
|
+
:label => label
|
322
|
+
)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
|
273
327
|
if (configuration.assume_first_custom_link_openurl &&
|
274
328
|
(first = record_xml.xpath "./CustomLinks/CustomLink" ) &&
|
275
329
|
(node = first.at_xpath "./Url" )
|
276
330
|
)
|
277
|
-
|
331
|
+
|
278
332
|
openurl = node.text
|
279
|
-
|
333
|
+
|
280
334
|
index = openurl.index('?')
|
281
|
-
item.openurl_kev_co = openurl.slice index..(openurl.length) if index
|
335
|
+
item.openurl_kev_co = openurl.slice index..(openurl.length) if index
|
282
336
|
end
|
283
337
|
|
284
|
-
# Format.
|
338
|
+
# Format.
|
285
339
|
item.format_str = at_xpath_text record_xml, "./Header/PubType"
|
286
340
|
# Can't find a list of possible PubTypes to see what's there to try
|
287
|
-
# and map to our internal controlled vocab. oh wells.
|
288
|
-
|
289
|
-
|
290
|
-
|
341
|
+
# and map to our internal controlled vocab. oh wells.
|
342
|
+
|
343
|
+
item.doi = at_xpath_text record_xml, "./RecordInfo/BibRecord/BibEntity/Identifiers/Identifier[child::Type[text()='doi']]/Value"
|
344
|
+
|
345
|
+
item.start_page = at_xpath_text(record_xml, "./RecordInfo/BibRecord/BibEntity/PhysicalDescription/Pagination/StartPage")
|
346
|
+
total_pages = at_xpath_text(record_xml, "./RecordInfo/BibRecord/BibEntity/PhysicalDescription/Pagination/PageCount")
|
347
|
+
if total_pages.to_i != 0 && item.start_page.to_i != 0
|
348
|
+
item.end_page = (item.start_page.to_i + total_pages.to_i - 1).to_s
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# location/call number, probably only for catalog results. We only see one
|
353
|
+
# in actual data, but XML structure allows multiple, so we'll store it as multiple.
|
354
|
+
copy_informations = record_xml.xpath("./Holdings/Holding/HoldingSimple/CopyInformationList/CopyInformation")
|
355
|
+
if copy_informations.present?
|
356
|
+
item.custom_data[:holdings] =
|
357
|
+
copy_informations.collect do |copy_information|
|
358
|
+
Holding.new(:location => at_xpath_text(copy_information, "Sublocation"),
|
359
|
+
:call_number => at_xpath_text(copy_information, "ShelfLocator"))
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
|
364
|
+
|
365
|
+
# For some EDS results, we have actual citation information,
|
366
|
+
# for some we don't.
|
367
|
+
container_xml = record_xml.at_xpath("./RecordInfo/BibRecord/BibRelationships/IsPartOfRelationships/IsPartOf/BibEntity")
|
368
|
+
if container_xml
|
369
|
+
item.source_title = at_xpath_text(container_xml, "./Titles/Title[child::Type[text()='main']]/TitleFull")
|
370
|
+
item.volume = at_xpath_text(container_xml, "./Numbering/Number[child::Type[text()='volume']]/Value")
|
371
|
+
item.issue = at_xpath_text(container_xml, "./Numbering/Number[child::Type[text()='issue']]/Value")
|
372
|
+
|
373
|
+
item.issn = at_xpath_text(container_xml, "./Identifiers/Identifier[child::Type[text()='issn-print']]/Value")
|
374
|
+
|
375
|
+
if date_xml = container_xml.at_xpath("./Dates/Date")
|
376
|
+
item.year = at_xpath_text(date_xml, "./Y")
|
377
|
+
|
378
|
+
date = at_xpath_text(date_xml, "./D").to_i
|
379
|
+
month = at_xpath_text(date_xml, "./M").to_i
|
380
|
+
if item.year.to_i != 0 && date != 0 && month != 0
|
381
|
+
item.publication_date = Date.new(item.year.to_i, month, date)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# EDS annoyingly repeats a monographic title in the same place
|
387
|
+
# we look for source/container title, take it away.
|
388
|
+
if item.start_page.blank? && helper.strip_tags(item.title) == item.source_title
|
389
|
+
item.source_title = nil
|
390
|
+
end
|
391
|
+
|
392
|
+
# Legacy EDS citation extracting. We don't really need this any more
|
393
|
+
# because EDS api has improved, but leave it in in case anyone using
|
394
|
+
# older versions needed it.
|
395
|
+
|
291
396
|
# We have a single blob of human-readable citation, that's also
|
292
397
|
# littered with XML-ish tags we need to deal with. We'll save
|
293
398
|
# it in a custom location, and use a custom Decorator to display
|
294
399
|
# it. Sorry it's way too hard for us to preserve <highlight>
|
295
400
|
# tags in this mess, they will be lost. Probably don't
|
296
|
-
# need highlighting in source anyhow.
|
401
|
+
# need highlighting in source anyhow.
|
297
402
|
citation_mess = element_by_group(record_xml, "Src")
|
298
403
|
# Argh, but sometimes it's in SrcInfo _without_ tags instead
|
299
|
-
if citation_mess
|
404
|
+
if citation_mess
|
300
405
|
citation_txt = Nokogiri::XML::fragment(citation_mess).text
|
301
406
|
# But strip off some "count of references" often on the end
|
302
|
-
# which are confusing and useless.
|
407
|
+
# which are confusing and useless.
|
303
408
|
item.custom_data["citation_blob"] = citation_txt.gsub(/ref +\d+ +ref\.$/, '')
|
304
409
|
else
|
305
410
|
# try another location
|
306
411
|
item.custom_data["citation_blob"] = element_by_group(record_xml, "SrcInfo")
|
307
412
|
end
|
308
|
-
|
309
|
-
|
413
|
+
|
310
414
|
item.extend CitationMessDecorator
|
311
|
-
|
415
|
+
|
312
416
|
results << item
|
313
|
-
end
|
417
|
+
end
|
314
418
|
end
|
315
|
-
|
316
|
-
return results
|
419
|
+
|
420
|
+
return results
|
317
421
|
rescue EdsCommException => e
|
318
422
|
results.error ||= {}
|
319
423
|
results.error[:exception] = e
|
@@ -321,137 +425,137 @@ class BentoSearch::EdsEngine
|
|
321
425
|
results.error[:http_body] = e.http_body
|
322
426
|
return results
|
323
427
|
end
|
324
|
-
|
428
|
+
|
325
429
|
end
|
326
|
-
|
430
|
+
|
327
431
|
# Difficult to get individual elements out of an EDS XML <Record>
|
328
|
-
# response, requires weird xpath, so we do it for you.
|
432
|
+
# response, requires weird xpath, so we do it for you.
|
329
433
|
# element_by_group(nokogiri_element, "Ti")
|
330
434
|
#
|
331
435
|
# Returns string or nil
|
332
436
|
def element_by_group(noko, group)
|
333
437
|
at_xpath_text(noko, "./Items/Item[child::Group[text()='#{group}']]/Data")
|
334
438
|
end
|
335
|
-
|
439
|
+
|
336
440
|
# Wraps calls to the EDS api with CreateSession and EndSession requests
|
337
441
|
# to EDS. Will pass sessionID in yield from block.
|
338
442
|
#
|
339
443
|
# Second optional arg is whether this is an authenticated user, else
|
340
|
-
# guest access will be used.
|
444
|
+
# guest access will be used.
|
341
445
|
#
|
342
446
|
# with_session(true) do |session_token|
|
343
447
|
# # can make more requests using session_token,
|
344
|
-
# # EndSession will be called for you at end of block.
|
448
|
+
# # EndSession will be called for you at end of block.
|
345
449
|
# end
|
346
450
|
def with_session(auth = false, &block)
|
347
|
-
auth_token = self.class.remembered_auth
|
451
|
+
auth_token = self.class.remembered_auth
|
348
452
|
if auth_token.nil?
|
349
453
|
auth_token = self.class.remembered_auth = get_auth_token
|
350
454
|
end
|
351
|
-
|
352
|
-
|
353
|
-
create_url = "#{configuration.base_url}createsession?profile=#{configuration.profile}&guest=#{auth ? 'n' : 'y'}"
|
354
|
-
response_xml = get_with_auth(create_url)
|
355
|
-
|
455
|
+
|
456
|
+
|
457
|
+
create_url = "#{configuration.base_url}createsession?profile=#{configuration.profile}&guest=#{auth ? 'n' : 'y'}"
|
458
|
+
response_xml = get_with_auth(create_url)
|
459
|
+
|
356
460
|
session_token = nil
|
357
|
-
unless response_xml && (session_token = at_xpath_text(response_xml, "//SessionToken"))
|
358
|
-
e = EdsCommException.new("Could not get SessionToken")
|
461
|
+
unless response_xml && (session_token = at_xpath_text(response_xml, "//SessionToken"))
|
462
|
+
e = EdsCommException.new("Could not get SessionToken")
|
359
463
|
end
|
360
|
-
|
361
|
-
begin
|
464
|
+
|
465
|
+
begin
|
362
466
|
block.yield(session_token)
|
363
|
-
ensure
|
364
|
-
if auth_token && session_token
|
467
|
+
ensure
|
468
|
+
if auth_token && session_token
|
365
469
|
end_url = "#{configuration.base_url}endsession?sessiontoken=#{CGI.escape session_token}"
|
366
|
-
response_xml = get_with_auth(end_url)
|
470
|
+
response_xml = get_with_auth(end_url)
|
367
471
|
end
|
368
472
|
end
|
369
|
-
|
473
|
+
|
370
474
|
end
|
371
|
-
|
372
|
-
# if the xpath responds, return #text of it, else nil.
|
475
|
+
|
476
|
+
# if the xpath responds, return #text of it, else nil.
|
373
477
|
def at_xpath_text(noko, xpath)
|
374
478
|
node = noko.at_xpath(xpath)
|
375
|
-
|
479
|
+
|
376
480
|
if node.nil?
|
377
481
|
return node
|
378
482
|
else
|
379
483
|
return node.text
|
380
484
|
end
|
381
485
|
end
|
382
|
-
|
486
|
+
|
383
487
|
# If EDS has put highlighting tags
|
384
488
|
# in a field, we need to HTML escape the literal values,
|
385
489
|
# while still using the highlighting tokens to put
|
386
490
|
# HTML tags around highlighted terms.
|
387
491
|
#
|
388
492
|
# Second param, if to assume EDS literals are safe HTML, as they
|
389
|
-
# seem to be.
|
493
|
+
# seem to be.
|
390
494
|
def prepare_eds_payload(str, html_safe = false)
|
391
495
|
return str if str.blank?
|
392
|
-
|
496
|
+
|
393
497
|
unless configuration.highlighting
|
394
|
-
str = str.html_safe if html_safe
|
498
|
+
str = str.html_safe if html_safe
|
395
499
|
return str
|
396
500
|
end
|
397
|
-
|
398
|
-
parts =
|
501
|
+
|
502
|
+
parts =
|
399
503
|
str.split(%r{(</?highlight>)}).collect do |substr|
|
400
504
|
case substr
|
401
505
|
when "<highlight>" then "<b class='bento_search_highlight'>".html_safe
|
402
506
|
when "</highlight>" then "</b>".html_safe
|
403
|
-
# Yes, EDS gives us HTML in the literals, we're choosing to trust it.
|
507
|
+
# Yes, EDS gives us HTML in the literals, we're choosing to trust it.
|
404
508
|
else substr.html_safe
|
405
509
|
end
|
406
510
|
end
|
407
|
-
|
408
|
-
return helper.safe_join(parts, '')
|
511
|
+
|
512
|
+
return helper.safe_join(parts, '')
|
409
513
|
end
|
410
|
-
|
514
|
+
|
411
515
|
# Give it a url pointing at EDS API.
|
412
|
-
# Second arg must be a session_token if EDS request requires one.
|
413
|
-
# It will
|
516
|
+
# Second arg must be a session_token if EDS request requires one.
|
517
|
+
# It will
|
414
518
|
# * Make a GET request
|
415
519
|
# * with memo-ized auth token added to headers
|
416
520
|
# * for XML, with all namespaces removed!
|
417
521
|
# * Parse JSON into a hash and return hash
|
418
522
|
# * Try ONCE more to get if EBSCO says bad auth token
|
419
523
|
# * Raise an EdsCommException if can't auth after second try,
|
420
|
-
# or other error message, or JSON can't be parsed.
|
524
|
+
# or other error message, or JSON can't be parsed.
|
421
525
|
def get_with_auth(url, session_token = nil)
|
422
526
|
auth_token = self.class.remembered_auth
|
423
527
|
unless auth_token
|
424
528
|
auth_token = self.class.remembered_auth = get_auth_token
|
425
529
|
end
|
426
|
-
|
530
|
+
|
427
531
|
response = nil
|
428
532
|
response_xml = nil
|
429
533
|
caught_exception = nil
|
430
|
-
|
534
|
+
|
431
535
|
begin
|
432
536
|
headers = {AuthHeader => auth_token, 'Accept' => 'application/xml'}
|
433
537
|
headers[SessionTokenHeader] = session_token if session_token
|
434
|
-
|
538
|
+
|
435
539
|
s_time = Time.now
|
436
540
|
response = http_client.get(url, nil, headers)
|
437
541
|
Rails.logger.debug("EDS timing GET: #{Time.now - s_time}:#{url}")
|
438
|
-
|
542
|
+
|
439
543
|
response_xml = Nokogiri::XML(response.body)
|
440
544
|
response_xml.remove_namespaces!
|
441
|
-
|
545
|
+
|
442
546
|
if (at_xpath_text(response_xml, "//ErrorNumber") == "104") || (at_xpath_text(response_xml, "//ErrorDescription") == "Auth Token Invalid")
|
443
547
|
# bad auth, try again just ONCE
|
444
548
|
Rails.logger.debug("EDS auth failed, getting auth again")
|
445
|
-
|
549
|
+
|
446
550
|
headers[AuthHeader] = self.class.remembered_auth = get_auth_token
|
447
551
|
response = http_client.get(url, nil, headers)
|
448
552
|
response_xml = Nokogiri::XML(response.body)
|
449
|
-
response_xml.remove_namespaces!
|
450
|
-
end
|
451
|
-
rescue
|
553
|
+
response_xml.remove_namespaces!
|
554
|
+
end
|
555
|
+
rescue BentoSearch::RubyTimeoutClass, HTTPClient::ConfigurationError, HTTPClient::BadResponseError, Nokogiri::SyntaxError => e
|
452
556
|
caught_exception = e
|
453
557
|
end
|
454
|
-
|
558
|
+
|
455
559
|
if response.nil? || response_xml.nil? || caught_exception || (! HTTP::Status.successful? response.status)
|
456
560
|
exception = EdsCommException.new("Error fetching URL: #{caught_exception.message if caught_exception} : #{url}")
|
457
561
|
if response
|
@@ -460,68 +564,68 @@ class BentoSearch::EdsEngine
|
|
460
564
|
end
|
461
565
|
raise exception
|
462
566
|
end
|
463
|
-
|
567
|
+
|
464
568
|
return response_xml
|
465
569
|
end
|
466
|
-
|
467
|
-
|
468
|
-
# Has to make an HTTP request to get EBSCO's auth token.
|
570
|
+
|
571
|
+
|
572
|
+
# Has to make an HTTP request to get EBSCO's auth token.
|
469
573
|
# returns the auth token. We aren't bothering to keep
|
470
574
|
# track of the expiration ourselves, can't neccesarily trust
|
471
|
-
# it anyway.
|
575
|
+
# it anyway.
|
472
576
|
#
|
473
|
-
# Raises an EdsCommException on error.
|
474
|
-
def get_auth_token
|
577
|
+
# Raises an EdsCommException on error.
|
578
|
+
def get_auth_token
|
475
579
|
# Can't send params as form-encoded, actually need to send a JSON or XML
|
476
|
-
# body, argh.
|
477
|
-
|
580
|
+
# body, argh.
|
581
|
+
|
478
582
|
body = <<-EOS
|
479
583
|
{
|
480
584
|
"UserId":"#{configuration.user_id}",
|
481
585
|
"Password":"#{configuration.password}"
|
482
586
|
}
|
483
587
|
EOS
|
484
|
-
|
588
|
+
|
485
589
|
s_time = Time.now
|
486
590
|
response = http_client.post(configuration.auth_url, body, {'Accept' => "application/json", "Content-type" => "application/json"})
|
487
|
-
Rails.logger.debug("EDS timing AUTH: #{Time.now - s_time}s")
|
488
|
-
|
591
|
+
Rails.logger.debug("EDS timing AUTH: #{Time.now - s_time}s")
|
592
|
+
|
489
593
|
unless HTTP::Status.successful? response.status
|
490
594
|
raise EdsCommException.new("Could not get auth", response.status, response.body)
|
491
595
|
end
|
492
|
-
|
596
|
+
|
493
597
|
response_hash = nil
|
494
598
|
begin
|
495
599
|
response_hash = MultiJson.load response.body
|
496
600
|
rescue MultiJson::DecodeError
|
497
601
|
end
|
498
|
-
|
602
|
+
|
499
603
|
unless response_hash.kind_of?(Hash) && response_hash.has_key?("AuthToken")
|
500
604
|
raise EdsCommException.new("AuthToken not found in auth response", response.status, response.body)
|
501
605
|
end
|
502
|
-
|
503
|
-
return response_hash["AuthToken"]
|
606
|
+
|
607
|
+
return response_hash["AuthToken"]
|
504
608
|
end
|
505
|
-
|
609
|
+
|
506
610
|
def self.default_configuration
|
507
611
|
{
|
508
612
|
:auth_url => 'https://eds-api.ebscohost.com/authservice/rest/uidauth',
|
509
613
|
:base_url => "http://eds-api.ebscohost.com/edsapi/rest/",
|
510
614
|
:highlighting => true,
|
511
615
|
:truncate_highlighted => 280,
|
512
|
-
:assume_first_custom_link_openurl =>
|
616
|
+
:assume_first_custom_link_openurl => false,
|
513
617
|
:search_mode => 'all' # any | bool | all | smart ; http://support.epnet.com/knowledge_base/detail.php?topic=996&id=1288&page=1
|
514
618
|
}
|
515
619
|
end
|
516
|
-
|
517
|
-
def sort_definitions
|
518
|
-
{
|
620
|
+
|
621
|
+
def sort_definitions
|
622
|
+
{
|
519
623
|
"date_desc" => {:implementation => "date"},
|
520
624
|
"relevance" => {:implementation => "relevance" }
|
521
625
|
# "date_asc" => {:implementaiton => "date2"}
|
522
626
|
}
|
523
627
|
end
|
524
|
-
|
628
|
+
|
525
629
|
def search_field_definitions
|
526
630
|
{
|
527
631
|
"TX" => {:semantic => :general},
|
@@ -534,11 +638,11 @@ class BentoSearch::EdsEngine
|
|
534
638
|
"IB" => {:semantic => :isbn},
|
535
639
|
}
|
536
640
|
end
|
537
|
-
|
538
|
-
# an exception talking to EDS api.
|
641
|
+
|
642
|
+
# an exception talking to EDS api.
|
539
643
|
# there's a short reason in #message, but also
|
540
644
|
# possibly an http_status and http_body copied
|
541
|
-
# from error EDS response.
|
645
|
+
# from error EDS response.
|
542
646
|
class EdsCommException < ::BentoSearch::FetchError
|
543
647
|
attr_accessor :http_status, :http_body
|
544
648
|
def initialize(message, status = nil, body = nil)
|
@@ -547,16 +651,32 @@ class BentoSearch::EdsEngine
|
|
547
651
|
self.http_body = body
|
548
652
|
end
|
549
653
|
end
|
550
|
-
|
551
|
-
|
654
|
+
|
655
|
+
|
552
656
|
# A built-in decorator alwasy applied, that over-rides
|
553
657
|
# the ResultItem#published_in display method to use our mess blob
|
554
658
|
# of human readable citation, since we don't have individual elements
|
555
|
-
# to create it from in a normalized way.
|
659
|
+
# to create it from in a normalized way.
|
556
660
|
module CitationMessDecorator
|
557
661
|
def published_in
|
558
662
|
custom_data["citation_blob"]
|
559
663
|
end
|
560
664
|
end
|
561
|
-
|
665
|
+
|
666
|
+
# a class that includes some Rails helper modules for
|
667
|
+
# text handling.
|
668
|
+
class Helper
|
669
|
+
include ActionView::Helpers::SanitizeHelper # for strip_tags
|
670
|
+
include ActionView::Helpers::TextHelper # for truncate
|
671
|
+
include ActionView::Helpers::OutputSafetyHelper # for safe_join
|
672
|
+
end
|
673
|
+
|
674
|
+
class Holding
|
675
|
+
attr_reader :location, :call_number
|
676
|
+
def initialize(args)
|
677
|
+
@location = args[:location]
|
678
|
+
@call_number = args[:call_number]
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
562
682
|
end
|