Class: BentoSearch::EdsEngine
- Inherits:
-
Object
- Object
- BentoSearch::EdsEngine
- Extended by:
- HTTPClientPatch::IncludeClient
- Includes:
- SearchEngine
- Defined in:
- app/search_engines/bento_search/eds_engine.rb
Overview
EDS docs:
-
Console App to demo requests: eds-api.ebscohost.com/Console
-
You’ll need to request an account to the EDS wiki, see: support.ebsco.com/knowledge_base/detail.php?id=5990
Defined Under Namespace
Modules: CitationMessDecorator Classes: EdsCommException
Constant Summary collapse
- HttpTimeout =
Can’t change http timeout in config, because we keep an http client at class-wide level, and config is not class-wide. Change this ‘constant’ if you want to change it, I guess.
4
- AuthHeader =
"x-authenticationToken"
- SessionTokenHeader =
"x-sessionToken"
- @@remembered_auth =
nil
- @@remembered_auth_lock =
Mutex.new
Constants included from SearchEngine
Class Method Summary collapse
- .default_configuration ⇒ Object
-
.remembered_auth ⇒ Object
Class variable to save current known good auth uses a mutex to be threadsafe.
-
.remembered_auth=(token) ⇒ Object
Set class variable with current known good auth.
- .required_configuration ⇒ Object
Instance Method Summary collapse
-
#at_xpath_text(noko, xpath) ⇒ Object
if the xpath responds, return #text of it, else nil.
-
#authenticated_end_user?(args) ⇒ Boolean
From config or args, args over-ride config.
- #construct_search_url(args) ⇒ Object
-
#element_by_group(noko, group) ⇒ Object
Difficult to get individual elements out of an EDS XML <Record> response, requires weird xpath, so we do it for you.
-
#get_auth_token ⇒ Object
Has to make an HTTP request to get EBSCO’s auth token.
-
#get_with_auth(url, session_token = nil) ⇒ Object
Give it a url pointing at EDS API.
-
#helper ⇒ Object
an object that includes some Rails helper modules for text handling.
-
#prepare_eds_payload(str, html_safe = false) ⇒ Object
If EDS has put highlighting tags in a field, we need to HTML escape the literal values, while still using the highlighting tokens to put HTML tags around highlighted terms.
- #search_field_definitions ⇒ Object
- #search_implementation(args) ⇒ Object
- #sort_definitions ⇒ Object
-
#with_session(auth = false, &block) ⇒ Object
Wraps calls to the EDS api with CreateSession and EndSession requests to EDS.
Methods included from HTTPClientPatch::IncludeClient
Methods included from SearchEngine
#fill_in_search_metadata_for, #initialize, #normalized_search_arguments, #public_settable_search_args, #search
Methods included from SearchEngine::Capabilities
#max_per_page, #search_keys, #semantic_search_keys, #semantic_search_map, #sort_keys
Class Method Details
.default_configuration ⇒ Object
506 507 508 509 510 511 512 513 514 515 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 506 def self.default_configuration { :auth_url => 'https://eds-api.ebscohost.com/authservice/rest/uidauth', :base_url => "http://eds-api.ebscohost.com/edsapi/rest/", :highlighting => true, :truncate_highlighted => 280, :assume_first_custom_link_openurl => true, :search_mode => 'all' # any | bool | all | smart ; http://support.epnet.com/knowledge_base/detail.php?topic=996&id=1288&page=1 } end |
.remembered_auth ⇒ Object
Class variable to save current known good auth uses a mutex to be threadsafe. sigh.
118 119 120 121 122 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 118 def self.remembered_auth @@remembered_auth_lock.synchronize do @@remembered_auth end end |
.remembered_auth=(token) ⇒ Object
Set class variable with current known good auth. uses a mutex to be threadsafe.
125 126 127 128 129 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 125 def self.remembered_auth=(token) @@remembered_auth_lock.synchronize do @@remembered_auth = token end end |
.required_configuration ⇒ Object
143 144 145 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 143 def self.required_configuration %w{user_id password profile} end |
Instance Method Details
#at_xpath_text(noko, xpath) ⇒ Object
if the xpath responds, return #text of it, else nil.
373 374 375 376 377 378 379 380 381 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 373 def at_xpath_text(noko, xpath) node = noko.at_xpath(xpath) if node.nil? return node else return node.text end end |
#authenticated_end_user?(args) ⇒ Boolean
From config or args, args over-ride config
148 149 150 151 152 153 154 155 156 157 158 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 148 def authenticated_end_user?(args) config = configuration.auth ? true : false arg = args[:auth] if ! arg.nil? arg ? true : false elsif ! config.nil? config ? true : false else false end end |
#construct_search_url(args) ⇒ Object
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 160 def construct_search_url(args) query = "AND," if args[:search_field] query += "#{args[:search_field]}:" end # Can't have any commas in query, it turns out, although # this is not documented. query += args[:query].gsub(",", " ") url = "#{configuration.base_url}search?view=detailed&query=#{CGI.escape query}" url += "&searchmode=#{CGI.escape configuration.search_mode}" url += "&highlight=#{configuration.highlighting ? 'y' : 'n' }" if args[:per_page] url += "&resultsperpage=#{args[:per_page]}" end if args[:page] url += "&pagenumber=#{args[:page]}" end if args[:sort] if (defn = self.sort_definitions[args[:sort]]) && (value = defn[:implementation] ) url += "&sort=#{CGI.escape value}" end end if configuration.only_source_types.present? # facetfilter=1,SourceType:Research Starters,SourceType:Books url += "&facetfilter=" + CGI.escape("1," + configuration.only_source_types.collect {|t| "SourceType:#{t}"}.join(",")) end return url end |
#element_by_group(noko, group) ⇒ Object
Difficult to get individual elements out of an EDS XML <Record> response, requires weird xpath, so we do it for you. element_by_group(nokogiri_element, “Ti”)
Returns string or nil
332 333 334 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 332 def element_by_group(noko, group) at_xpath_text(noko, "./Items/Item[child::Group[text()='#{group}']]/Data") end |
#get_auth_token ⇒ Object
Has to make an HTTP request to get EBSCO’s auth token. returns the auth token. We aren’t bothering to keep track of the expiration ourselves, can’t neccesarily trust it anyway.
Raises an EdsCommException on error.
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 474 def get_auth_token # Can't send params as form-encoded, actually need to send a JSON or XML # body, argh. body = <<-EOS { "UserId":"#{configuration.user_id}", "Password":"#{configuration.password}" } EOS s_time = Time.now response = http_client.post(configuration.auth_url, body, {'Accept' => "application/json", "Content-type" => "application/json"}) Rails.logger.debug("EDS timing AUTH: #{Time.now - s_time}s") unless HTTP::Status.successful? response.status raise EdsCommException.new("Could not get auth", response.status, response.body) end response_hash = nil begin response_hash = MultiJson.load response.body rescue MultiJson::DecodeError end unless response_hash.kind_of?(Hash) && response_hash.has_key?("AuthToken") raise EdsCommException.new("AuthToken not found in auth response", response.status, response.body) end return response_hash["AuthToken"] end |
#get_with_auth(url, session_token = nil) ⇒ Object
Give it a url pointing at EDS API. Second arg must be a session_token if EDS request requires one. It will
-
Make a GET request
-
with memo-ized auth token added to headers
-
for XML, with all namespaces removed!
-
Parse JSON into a hash and return hash
-
Try ONCE more to get if EBSCO says bad auth token
-
Raise an EdsCommException if can’t auth after second try, or other error message, or JSON can’t be parsed.
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 421 def get_with_auth(url, session_token = nil) auth_token = self.class.remembered_auth unless auth_token auth_token = self.class.remembered_auth = get_auth_token end response = nil response_xml = nil caught_exception = nil begin headers = {AuthHeader => auth_token, 'Accept' => 'application/xml'} headers[SessionTokenHeader] = session_token if session_token s_time = Time.now response = http_client.get(url, nil, headers) Rails.logger.debug("EDS timing GET: #{Time.now - s_time}:#{url}") response_xml = Nokogiri::XML(response.body) response_xml.remove_namespaces! if (at_xpath_text(response_xml, "//ErrorNumber") == "104") || (at_xpath_text(response_xml, "//ErrorDescription") == "Auth Token Invalid") # bad auth, try again just ONCE Rails.logger.debug("EDS auth failed, getting auth again") headers[AuthHeader] = self.class.remembered_auth = get_auth_token response = http_client.get(url, nil, headers) response_xml = Nokogiri::XML(response.body) response_xml.remove_namespaces! end rescue TimeoutError, HTTPClient::ConfigurationError, HTTPClient::BadResponseError, Nokogiri::SyntaxError => e caught_exception = e end if response.nil? || response_xml.nil? || caught_exception || (! HTTP::Status.successful? response.status) exception = EdsCommException.new("Error fetching URL: #{caught_exception. if caught_exception} : #{url}") if response exception.http_body = response.body exception.http_status = response.status end raise exception end return response_xml end |
#helper ⇒ Object
an object that includes some Rails helper modules for text handling.
133 134 135 136 137 138 139 140 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 133 def helper unless @helper @helper = Object.new @helper.extend ActionView::Helpers::TextHelper # for truncate @helper.extend ActionView::Helpers::OutputSafetyHelper # for safe_join end return @helper end |
#prepare_eds_payload(str, html_safe = false) ⇒ Object
If EDS has put highlighting tags in a field, we need to HTML escape the literal values, while still using the highlighting tokens to put HTML tags around highlighted terms.
Second param, if to assume EDS literals are safe HTML, as they seem to be.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 390 def prepare_eds_payload(str, html_safe = false) return str if str.blank? unless configuration.highlighting str = str.html_safe if html_safe return str end parts = str.split(%r{(</?highlight>)}).collect do |substr| case substr when "<highlight>" then "<b class='bento_search_highlight'>".html_safe when "</highlight>" then "</b>".html_safe # Yes, EDS gives us HTML in the literals, we're choosing to trust it. else substr.html_safe end end return helper.safe_join(parts, '') end |
#search_field_definitions ⇒ Object
525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 525 def search_field_definitions { "TX" => {:semantic => :general}, "AU" => {:semantic => :author}, "TI" => {:semantic => :title}, "SU" => {:semantic => :subject}, "SO" => {}, # source, journal name "AB" => {}, # abstract "IS" => {:semantic => :issn}, "IB" => {:semantic => :isbn}, } end |
#search_implementation(args) ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 200 def search_implementation(args) results = BentoSearch::Results.new end_user_auth = authenticated_end_user? args begin with_session(end_user_auth) do |session_token| url = construct_search_url(args) response = get_with_auth(url, session_token) results = BentoSearch::Results.new if (hits_node = at_xpath_text(response, "./SearchResponseMessageGet/SearchResult/Statistics/TotalHits")) results.total_items = hits_node.to_i end response.xpath("./SearchResponseMessageGet/SearchResult/Data/Records/Record").each do |record_xml| item = BentoSearch::ResultItem.new item.title = prepare_eds_payload( element_by_group(record_xml, "Ti"), true ) # To get a unique id, we need to pull out db code and accession number # and combine em with colon, accession number is not unique by itself. db = record_xml.at_xpath("./Header/DbId").try(:text) accession = record_xml.at_xpath("./Header/An").try(:text) if db && accession item.unique_id = "#{db}:#{accession}" end if item.title.nil? && ! end_user_auth item.title = I18n.translate("bento_search.eds.record_not_available") end item.abstract = prepare_eds_payload( element_by_group(record_xml, "Ab"), true ) # Believe it or not, the authors are encoded as an escaped # XML-ish payload, that we need to parse again and get the # actual authors out of. WTF. Thanks for handling fragments # nokogiri. = element_by_group(record_xml, "Au") # only SOMETIMES does it have XML tags, other times it's straight text. # ARGH. = Nokogiri::XML::fragment() searchLinks = .xpath(".//searchLink") if searchLinks.size > 0 .xpath(".//searchLink").each do || item. << BentoSearch::Author.new(:display => .text) end else item. << BentoSearch::Author.new(:display => .text) end # PLink is main inward facing EBSCO link, put it as # main link. if direct_link = record_xml.at_xpath("./PLink") item.link = direct_link.text end # Other links may be found in CustomLinks, it seems like usually # there will be at least one, hopefully the first one is the OpenURL? record_xml.xpath("./CustomLinks/CustomLink").each do |custom_link| item.other_links << BentoSearch::Link.new( :url => custom_link.at_xpath("./Url").text, :label => custom_link.at_xpath("./Name").text ) end if (configuration.assume_first_custom_link_openurl && (first = record_xml.xpath "./CustomLinks/CustomLink" ) && (node = first.at_xpath "./Url" ) ) openurl = node.text index = openurl.index('?') item.openurl_kev_co = openurl.slice index..(openurl.length) if index end # Format. item.format_str = at_xpath_text record_xml, "./Header/PubType" # Can't find a list of possible PubTypes to see what's there to try # and map to our internal controlled vocab. oh wells. # We have a single blob of human-readable citation, that's also # littered with XML-ish tags we need to deal with. We'll save # it in a custom location, and use a custom Decorator to display # it. Sorry it's way too hard for us to preserve <highlight> # tags in this mess, they will be lost. Probably don't # need highlighting in source anyhow. citation_mess = element_by_group(record_xml, "Src") # Argh, but sometimes it's in SrcInfo _without_ tags instead if citation_mess citation_txt = Nokogiri::XML::fragment(citation_mess).text # But strip off some "count of references" often on the end # which are confusing and useless. item.custom_data["citation_blob"] = citation_txt.gsub(/ref +\d+ +ref\.$/, '') else # try another location item.custom_data["citation_blob"] = element_by_group(record_xml, "SrcInfo") end item.extend CitationMessDecorator results << item end end return results rescue EdsCommException => e results.error ||= {} results.error[:exception] = e results.error[:http_status] = e.http_status results.error[:http_body] = e.http_body return results end end |
#sort_definitions ⇒ Object
517 518 519 520 521 522 523 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 517 def sort_definitions { "date_desc" => {:implementation => "date"}, "relevance" => {:implementation => "relevance" } # "date_asc" => {:implementaiton => "date2"} } end |
#with_session(auth = false, &block) ⇒ Object
Wraps calls to the EDS api with CreateSession and EndSession requests to EDS. Will pass sessionID in yield from block.
Second optional arg is whether this is an authenticated user, else guest access will be used.
with_session(true) do |session_token|
# can make more requests using session_token,
# EndSession will be called for you at end of block.
end
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'app/search_engines/bento_search/eds_engine.rb', line 346 def with_session(auth = false, &block) auth_token = self.class.remembered_auth if auth_token.nil? auth_token = self.class.remembered_auth = get_auth_token end create_url = "#{configuration.base_url}createsession?profile=#{configuration.profile}&guest=#{auth ? 'n' : 'y'}" response_xml = get_with_auth(create_url) session_token = nil unless response_xml && (session_token = at_xpath_text(response_xml, "//SessionToken")) e = EdsCommException.new("Could not get SessionToken") end begin block.yield(session_token) ensure if auth_token && session_token end_url = "#{configuration.base_url}endsession?sessiontoken=#{CGI.escape session_token}" response_xml = get_with_auth(end_url) end end end |