require 'ebsco/eds/version'
require 'ebsco/eds/info'
require 'ebsco/eds/results'
require 'net/http/persistent'
require 'faraday'
require 'faraday/detailed_logger'
require 'faraday_middleware'
require 'faraday/adapter/net_http_persistent'
require 'faraday_eds_middleware'
require 'logger'
require 'json'
require 'active_support'
require 'ebsco/eds/configuration'
require 'digest/md5'
module EBSCO
module EDS
# Sessions are used to query and retrieve information from the EDS API.
class Session
# Contains search Info available in the session profile. Includes the sort options, search fields, limiters and expanders available to the profile.
attr_accessor :info
# Contains the search Options sent as part of each request to the EDS API such as limiters, expanders, search modes, sort order, etc.
attr_accessor :search_options
# The authentication token. This is passed along in the x-authenticationToken HTTP header.
attr_accessor :auth_token # :nodoc:
# The session token. This is passed along in the x-sessionToken HTTP header.
attr_accessor :session_token # :nodoc:
attr_accessor :citation_token # :nodoc:
# The session configuration.
attr_reader :config
# Creates a new session.
#
# This can be done in one of two ways:
#
# === 1. Environment variables
# * +EDS_AUTH+ - authentication method: 'ip' or 'user'
# * +EDS_PROFILE+ - profile ID for the EDS API
# * +EDS_USER+ - user id attached to the profile
# * +EDS_PASS+ - user password
# * +EDS_GUEST+ - allow guest access: 'y' or 'n'
# * +EDS_ORG+ - name of your institution or company
#
# ==== Example
# Once you have environment variables set, simply create a session like this:
# session = EBSCO::EDS::Session.new
#
# === 2. \Options
# * +:auth+
# * +:profile+
# * +:user+
# * +:pass+
# * +:guest+
# * +:org+
#
# ==== Example
# session = EBSCO::EDS::Session.new {
# :auth => 'user',
# :profile => 'edsapi',
# :user => 'joe'
# :pass => 'secret',
# :guest => false,
# :org => 'Acme University'
# }
def initialize(options = {})
@session_token = ''
@citation_token = ''
@auth_token = ''
@config = {}
@guest = true
@api_hosts_list = ''
@api_host_index = 0
eds_config = EBSCO::EDS::Configuration.new
if options[:config]
@config = eds_config.configure_with(options[:config])
# return default if there is some problem with the yaml file (bad syntax, not found, etc.)
@config = eds_config.configure if @config.nil?
else
@config = eds_config.configure(options)
end
# these properties aren't in the config
if options.has_key? :user
@user = options[:user]
elsif ENV.has_key? 'EDS_USER'
@user = ENV['EDS_USER']
end
if options.has_key? :pass
@pass = options[:pass]
elsif ENV.has_key? 'EDS_PASS'
@pass = ENV['EDS_PASS']
end
if options.has_key? :profile
@profile = options[:profile]
elsif ENV.has_key? 'EDS_PROFILE'
@profile = ENV['EDS_PROFILE']
end
raise EBSCO::EDS::InvalidParameter, 'Session must specify a valid api profile.' if blank?(@profile)
# these config options can be overridden by environment vars
@auth_type = (ENV.has_key? 'EDS_AUTH') ? ENV['EDS_AUTH'] : @config[:auth]
@org = (ENV.has_key? 'EDS_ORG') ? ENV['EDS_ORG'] : @config[:org]
@cache_dir = (ENV.has_key? 'EDS_CACHE_DIR') ? ENV['EDS_CACHE_DIR'] : @config[:eds_cache_dir]
@log_level = (ENV.has_key? 'EDS_LOG_LEVEL') ? ENV['EDS_LOG_LEVEL'] : @config[:log_level]
(ENV.has_key? 'EDS_GUEST') ?
if %w(n N no No false False).include?(ENV['EDS_GUEST'])
@guest = false
else
@guest = true
end :
@guest = @config[:guest]
(ENV.has_key? 'EDS_USE_CACHE') ?
if %w(n N no No false False).include?(ENV['EDS_USE_CACHE'])
@use_cache = false
else
@use_cache = true
end :
@use_cache = @config[:use_cache]
(ENV.has_key? 'EDS_DEBUG') ?
if %w(y Y yes Yes true True).include?(ENV['EDS_DEBUG'])
@debug = true
else
@debug = false
end :
@debug = @config[:debug]
(ENV.has_key? 'EDS_HOSTS') ? @api_hosts_list = ENV['EDS_HOSTS'].split(',') : @api_hosts_list = @config[:api_hosts_list]
(ENV.has_key? 'EDS_RECOVER_FROM_BAD_SOURCE_TYPE') ?
if %w(y Y yes Yes true True).include?(ENV['EDS_RECOVER_FROM_BAD_SOURCE_TYPE'])
@recover_130 = true
else
@recover_130 = false
end :
@recover_130 = @config[:recover_from_bad_source_type]
# use cache for auth token, info, search and retrieve calls?
if @use_cache
cache_dir = File.join(@cache_dir, 'faraday_eds_cache')
@cache_store = ActiveSupport::Cache::FileStore.new cache_dir
end
@max_retries = @config[:max_attempts]
if options.has_key? :auth_token
@auth_token = options[:auth_token]
else
@auth_token = create_auth_token
end
if options.key? :session_token
@session_token = options[:session_token]
else
@session_token = create_session_token
end
if options.key? :citation_token
@citation_token = options[:citation_token]
else
@citation_token = create_citation_token
end
@info = EBSCO::EDS::Info.new(do_request(:get, path: @config[:info_url]), @config)
@current_page = 0
@search_options = nil
if @debug
if options.key? :caller
puts '*** CREATE SESSION CALLER: ' + options[:caller].inspect
puts '*** CALLER OPTIONS: ' + options.inspect
end
puts '*** AUTH TOKEN: ' + @auth_token.inspect
puts '*** SESSION TOKEN: ' + @session_token.inspect
puts '*** CITATION TOKEN: ' + @citation_token.inspect
end
end
# :category: Search & Retrieve Methods
# Performs a search.
#
# Returns search Results.
#
# ==== \Options
#
# * +:query+ - Required. The search terms. Format: {booleanOperator},{fieldCode}:{term}. Example: SU:Hiking
# * +:mode+ - Search mode to be used. Either: all (default), any, bool, smart
# * +:results_per_page+ - The number of records retrieved with the search results (between 1-100, default is 20).
# * +:page+ - Starting page number for the result set returned from a search (if results per page = 10, and page number = 3 , this implies: I am expecting 10 records starting at page 3).
# * +:sort+ - The sort order for the search results. Either: relevance (default), oldest, newest
# * +:highlight+ - Specifies whether or not the search term is highlighted using tags. Either true or false.
# * +:include_facets+ - Specifies whether or not the search term is highlighted using tags. Either true (default) or false.
# * +:facet_filters+ - Facets to apply to the search. Facets are used to refine previous search results. Format: \{filterID},{facetID}:{value}[,{facetID}:{value}]* Example: 1,SubjectEDS:food,SubjectEDS:fiction
# * +:view+ - Specifies the amount of data to return with the response. Either 'title': title only; 'brief' (default): Title + Source, Subjects; 'detailed': Brief + full abstract
# * +:actions+ - Actions to take on the existing query specification. Example: addfacetfilter(SubjectGeographic:massachusetts)
# * +:limiters+ - Criteria to limit the search results by. Example: LA99:English,French,German
# * +:expanders+ - Expanders that can be applied to the search. Either: thesaurus, fulltext, relatedsubjects
# * +:publication_id+ - Publication to search within.
# * +:related_content+ - Comma separated list of related content types to return with the search results. Either 'rs' (Research Starters) or 'emp' (Exact Publication Match)
# * +:auto_suggest+ - Specifies whether or not to return search suggestions along with the search results. Either true or false (default).
#
# ==== Examples
#
# results = session.search({query: 'abraham lincoln', results_per_page: 5, related_content: ['rs','emp']})
# results = session.search({query: 'volcano', results_per_page: 1, publication_id: 'eric', include_facets: false})
def search(options = {}, add_actions = false, increment_page = true)
# use existing/updated SearchOptions
if options.empty?
if @search_options.nil?
@search_results = EBSCO::EDS::Results.new(empty_results,@config)
else
_response = do_request(:post, path: '/edsapi/rest/Search', payload: @search_options)
@search_results = EBSCO::EDS::Results.new(_response, @config,
@info.available_limiters, options)
if increment_page
@current_page = @search_results.page_number
end
@search_results
end
else
# Only perform a search when there are query terms since certain EDS profiles will throw errors when
# given empty queries
if (options.keys & %w[query q]).any? || options.has_key?(:query)
# create/recreate the search options if nil or not passing actions
if @search_options.nil? || !add_actions
@search_options = EBSCO::EDS::Options.new(options, @info)
end
_response = do_request(:post, path: '/edsapi/rest/Search', payload: @search_options)
@search_results = EBSCO::EDS::Results.new(_response, @config,
@info.available_limiters, options)
# create temp format facet results if needed
if options['f']
if options['f'].key?('eds_publication_type_facet')
format_options = options.dup
format_options['f'] = options['f'].except('eds_publication_type_facet')
format_search_options = EBSCO::EDS::Options.new(format_options, @info)
format_search_options.Comment = 'temp source type facets'
_format_response = do_request(:post, path: '/edsapi/rest/Search', payload: format_search_options)
@search_results.temp_format_facet_results = EBSCO::EDS::Results.new(_format_response,
@config,
@info.available_limiters,
format_options)
end
end
# create temp content provider facet results if needed
if options['f']
if options['f'].key?('eds_content_provider_facet')
content_options = options.dup
content_options['f'] = options['f'].except('eds_content_provider_facet')
content_search_options = EBSCO::EDS::Options.new(content_options, @info)
content_search_options.Comment = 'temp content provider facet'
_content_response = do_request(:post, path: '/edsapi/rest/Search', payload: content_search_options)
@search_results.temp_content_provider_facet_results = EBSCO::EDS::Results.new(_content_response,
@config,
@info.available_limiters,
content_options)
end
end
if increment_page
@current_page = @search_results.page_number
end
@search_results
else
@search_results = EBSCO::EDS::Results.new(empty_results, @config)
end
end
end
# :category: Search & Retrieve Methods
# Performs a simple search. All other search options assume default values.
#
# Returns search Results.
#
# ==== Attributes
#
# * +query+ - the search query.
#
# ==== Examples
#
# results = session.simple_search('volcanoes')
#
def simple_search(query)
search({:query => query})
end
# :category: Search & Retrieve Methods
# Returns a Record based a particular result based on a database ID and accession number.
#
# ==== Attributes
#
# * +:dbid+ - The database ID (required).
# * +:an+ - The accession number (required).
# * +highlight+ - Comma separated list of terms to highlight in the data records (optional).
# * +ebook+ - Preferred format to return ebook content in. Either ebook-pdf (default) or ebook-pdf.
#
# ==== Examples
# record = session.retrieve({dbid: 'asn', an: '108974507'})
#
def retrieve(dbid:, an:, highlight: nil, ebook: 'ebook-pdf')
payload = { DbId: dbid, An: an, HighlightTerms: highlight, EbookPreferredFormat: ebook }
retrieve_response = do_request(:post, path: @config[:retrieve_url], payload: payload)
record = EBSCO::EDS::Record.new(retrieve_response, @config)
record_citation_exports = get_citation_exports({dbid: dbid, an: an, format: @config[:citation_exports_formats]})
unless record_citation_exports.nil?
record.set_citation_exports(record_citation_exports)
end
record_citation_styles = get_citation_styles({dbid: dbid, an: an, format: @config[:citation_styles_formats]})
unless record_citation_styles.nil?
record.set_citation_styles(record_citation_styles)
end
record
end
# fetch the citation from the citation rest endpoint
def get_citation_exports(dbid:, an:, format: 'all')
begin
# only available as non-guest otherwise 148 error
citation_exports_params = "?an=#{an}&dbid=#{dbid}&format=#{format}"
citation_exports_response = do_request(:get, path: @config[:citation_exports_url] + citation_exports_params)
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: citation_exports_response, eds_config: @config)
rescue EBSCO::EDS::BadRequest => e
custom_error_message = JSON.parse e.message.gsub('=>', ':')
# ErrorNumber 112 - Invalid Argument Value
# ErrorNumber 132 - Record not found
if custom_error_message['ErrorNumber'] == '112'
unknown_export_format = {"Format"=>format, "Label"=>"", "Data"=>"", "Error"=>"Invalid citation export format"}
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: unknown_export_format, eds_config: @config)
elsif custom_error_message['ErrorNumber'] == '132'
record_not_found = {"Format"=>format, "Label"=>"", "Data"=>"", "Error"=>"Record not found"}
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: record_not_found, eds_config: @config)
else
unknown_error = {"Format"=>format, "Label"=>"", "Data"=>"", "Error"=>custom_error_message['ErrorDescription']}
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: unknown_error, eds_config: @config)
end
end
end
# fetch the citation from the citation rest endpoint
def get_citation_styles(dbid:, an:, format: 'all')
begin
citation_styles_params = "?an=#{an}&dbid=#{dbid}&styles=#{format}"
citation_styles_response = do_request(:get, path: @config[:citation_styles_url] + citation_styles_params)
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: citation_styles_response, eds_config: @config)
rescue EBSCO::EDS::BadRequest => e
custom_error_message = JSON.parse e.message.gsub('=>', ':')
unknown_error = {"Id"=>format, "Label"=>"", "Data"=>"", "Error"=>custom_error_message['ErrorDescription']}
EBSCO::EDS::Citations.new(dbid: dbid, an: an, citation_result: unknown_error, eds_config: @config)
end
end
# get citation styles for a list of result ids
def get_citation_styles_list(id_list: [], format: 'all')
citations = []
if id_list.any?
id_list.each { |id|
dbid = id.split('__',2).first
accession = id.split('__',2).last
citations.push get_citation_styles(dbid: dbid, an: accession, format: format)
}
end
citations
end
# get citation exports for a list of result ids
def get_citation_exports_list(id_list: [], format: 'all')
citations = []
if id_list.any?
id_list.each { |id|
dbid = id.split('__',2).first
accession = id.split('__',2).last
citations.push get_citation_exports(dbid: dbid, an: accession, format: format)
}
end
citations
end
# Create a result set with just the record before and after the current detailed record
def solr_retrieve_previous_next(options = {})
rid = options['previous-next-index']
# set defaults if missing
if options['page'].nil?
options['page'] = '1'
end
if options['per_page'].nil?
options['per_page'] = '20'
end
rpp = options['per_page'].to_i
# determine result page and update options
goto_page = rid / rpp
if (rid % rpp) > 0
goto_page += 1
end
options['page'] = goto_page.to_s
pnum = options['page'].to_i
max = rpp * pnum
min = max - rpp + 1
result_index = rid - min
cached_results = search(options, false, false)
cached_results_found = cached_results.stat_total_hits
# last result in set, get next result
if rid == max
options_next = options
options_next['page'] = cached_results.page_number+1
next_result_set = search(options_next, false, false)
result_next = next_result_set.records.first
else
unless rid == cached_results_found
result_next = cached_results.records[result_index+1]
end
end
if result_index == 0
# first result in set that's not the very first result, get previous result
if rid != 1
options_previous = options
options_previous['page'] = cached_results.page_number-1
previous_result_set = search(options_previous, false, false)
result_prev = previous_result_set.records.last
end
else
result_prev = cached_results.records[result_index-1]
end
# return json result set with just the previous and next records in it
r = empty_results(cached_results.stat_total_hits)
results = EBSCO::EDS::Results.new(r, @config)
next_previous_records = []
unless result_prev.nil?
next_previous_records << result_prev
end
unless result_next.nil?
next_previous_records << result_next
end
results.records = next_previous_records
results.to_solr
end
def solr_retrieve_list(list: [], highlight: nil)
records = []
if list.any?
list.each.with_index(1) { |id, index|
dbid = id.split('__',2).first
accession = id.split('__',2).last
current_rec = retrieve(dbid: dbid, an: accession, highlight: highlight, ebook: @config[:ebook_preferred_format])
current_rec.eds_result_id = index
records.push current_rec
}
end
r = empty_results(records.length)
results = EBSCO::EDS::Results.new(r, @config)
results.records = records
results.to_solr
end
# :category: Search & Retrieve Methods
# Invalidates the session token. End Session should be called when you know a user has logged out.
def end
# todo: catch when there is no valid session?
do_request(:post, path: @config[:end_session_url], payload: {:SessionToken => @session_token})
connection.headers['x-sessionToken'] = ''
@session_token = ''
end
# :category: Search & Retrieve Methods
# Clear all specified query expressions, facet filters, limiters and expanders, and set the page number back to 1.
# Returns search Results.
def clear_search
add_actions 'ClearSearch()'
end
# :category: Search & Retrieve Methods
# Clears all queries and facet filters, and set the page number back to 1; limiters and expanders are not modified.
# Returns search Results.
def clear_queries
add_actions 'ClearQueries()'
end
# :category: Search & Retrieve Methods
# Add a query to the search request. When a query is added, it will be assigned an ordinal, which will be exposed
# in the search response message. It also removes any specified facet filters and sets the page number to 1.
# Returns search Results.
# ==== Examples
# results = session.add_query('AND,California')
def add_query(query)
add_actions "AddQuery(#{query})"
end
# :category: Search & Retrieve Methods
# Removes query from the currently specified search. It also removes any specified facet filters and sets the
# page number to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_query(1)
def remove_query(query_id)
add_actions "removequery(#{query_id})"
end
# :category: Search & Retrieve Methods
# Add actions to an existing search session
# Returns search Results.
# ==== Examples
# results = session.add_actions('addfacetfilter(SubjectGeographic:massachusetts)')
def add_actions(actions)
@search_options.add_actions(actions, @info)
search()
end
# :category: Setter Methods
# Sets the sort for the search. The available sorts for the specified databases can be obtained from the session’s
# info attribute. Sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.set_sort('newest')
def set_sort(val)
add_actions "SetSort(#{val})"
end
# :category: Setter Methods
# Sets the search mode. The available search modes are returned from the info method.
# Returns search Results.
# ==== Examples
# results = session.set_search_mode('bool')
def set_search_mode(mode)
add_actions "SetSearchMode(#{mode})"
end
# :category: Setter Methods
# Specifies the view parameter. The view representes the amount of data to return with the search.
# Returns search Results.
# ==== Examples
# results = session.set_view('detailed')
def set_view(view)
add_actions "SetView(#{view})"
end
# :category: Setter Methods
# Sets whether or not to turn highlighting on or off (y|n).
# Returns search Results.
# ==== Examples
# results = session.set_highlight('n')
def set_highlight(val)
add_actions "SetHighlight(#{val})"
end
# :category: Setter Methods
# Sets the page size on the search request.
# Returns search Results.
# ==== Examples
# results = session.results_per_page(50)
def results_per_page(num)
add_actions "SetResultsPerPage(#{num})"
end
# :category: Setter Methods
# A related content type to additionally search for and include with the search results.
# Returns search Results.
# ==== Examples
# results = session.include_related_content('rs')
def include_related_content(val)
add_actions "includerelatedcontent(#{val})"
end
# Not available currently.
# TODO: ask for this to be added for consistency with other criteria
# def set_include_image_quick_view(val)
# add_actions "includeimagequickview(#{val})"
# end
# :category: Setter Methods
# Specify to include facets in the results or not. Either 'y' or 'n'.
# Returns search Results.
# ==== Examples
# results = session.set_include_facets('n')
def set_include_facets(val)
add_actions "SetIncludeFacets(#{val})"
end
# --
# ====================================================================================
# PAGINATION
# ====================================================================================
# ++
# :category: Pagination Methods
# Get the next page of results.
# Returns search Results.
def next_page
page = @current_page + 1
get_page(page)
end
# :category: Pagination Methods
# Get the previous page of results.
# Returns search Results.
def prev_page
get_page([1, @current_page - 1].sort.last)
end
# :category: Pagination Methods
# Get a specified page of results
# Returns search Results.
def get_page(page = 1)
add_actions "GoToPage(#{page})"
end
# :category: Pagination Methods
# Increments the current results page number by the value specified. If the current page was 5 and the specified value
# was 2, the page number would be set to 7.
# Returns search Results.
def move_page(num)
add_actions "MovePage(#{num})"
end
# :category: Pagination Methods
# Get the first page of results.
# Returns search Results.
def reset_page
add_actions 'ResetPaging()'
end
# --
# ====================================================================================
# FACETS
# ====================================================================================
# ++
# :category: Facet Methods
# Removes all specified facet filters. Sets the page number back to 1.
# Returns search Results.
def clear_facets
add_actions 'ClearFacetFilters()'
end
# :category: Facet Methods
# Adds a facet filter to the search request. Sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.add_facet('Publisher', 'wiley-blackwell')
# results = session.add_facet('SubjectEDS', 'water quality')
#
def add_facet(facet_id, facet_val)
facet_val = eds_sanitize(facet_val)
add_actions "AddFacetFilter(#{facet_id}:#{facet_val})"
end
# :category: Facet Methods
# Removes a specified facet filter id. Sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_facet(45)
def remove_facet(group_id)
add_actions "RemoveFacetFilter(#{group_id})"
end
# :category: Facet Methods
# Removes a specific facet filter value from a group. Sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_facet_value(2, 'DE', 'Psychology')
def remove_facet_value(group_id, facet_id, facet_val)
add_actions "RemoveFacetFilterValue(#{group_id},#{facet_id}:#{facet_val})"
end
# --
# ====================================================================================
# LIMITERS
# ====================================================================================
# ++
# :category: Limiter Methods
# Clears all currently specified limiters and sets the page number back to 1.
# Returns search Results.
def clear_limiters
add_actions 'ClearLimiters()'
end
# :category: Limiter Methods
# Adds a limiter to the currently defined search and sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.add_limiter('FT','y')
def add_limiter(id, val)
add_actions "AddLimiter(#{id}:#{val})"
end
# :category: Limiter Methods
# Removes the specified limiter and sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_limiter('FT')
def remove_limiter(id)
add_actions "RemoveLimiter(#{id})"
end
# :category: Limiter Methods
# Removes a specified limiter value and sets the page number back to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_limiter_value('LA99','French')
def remove_limiter_value(id, val)
add_actions "RemoveLimiterValue(#{id}:#{val})"
end
# --
# ====================================================================================
# EXPANDERS
# ====================================================================================
# ++
# :category: Expander Methods
# Removes all specified expanders and sets the page number back to 1.
# Returns search Results.
def clear_expanders
add_actions 'ClearExpanders()'
end
# :category: Expander Methods
# Adds expanders and sets the page number back to 1. Multiple expanders should be comma separated.
# Returns search Results.
# ==== Examples
# results = session.add_expander('thesaurus,fulltext')
def add_expander(val)
add_actions "AddExpander(#{val})"
end
# :category: Expander Methods
# Removes a specified expander. Sets the page number to 1.
# Returns search Results.
# ==== Examples
# results = session.remove_expander('fulltext')
def remove_expander(val)
add_actions "RemoveExpander(#{val})"
end
# --
# ====================================================================================
# PUBLICATION (this is only used for profiles configured for publication searching)
# ====================================================================================
# ++
# :category: Publication Methods
# Specifies a publication to search within. Sets the pages number back to 1.
# Returns search Results.
# ==== Examples
# results = session.add_publication('eric')
def add_publication(pub_id)
add_actions "AddPublication(#{pub_id})"
end
# :category: Publication Methods
# Removes a publication from the search. Sets the page number back to 1.
# Returns search Results.
def remove_publication(pub_id)
add_actions "RemovePublication(#{pub_id})"
end
# :category: Publication Methods
# Removes all publications from the search. Sets the page number back to 1.
# Returns search Results.
def remove_all_publications
add_actions 'RemovePublication()'
end
# --
# ====================================================================================
# INTERNAL METHODS
# ====================================================================================
# ++
def do_request(method, path:, payload: nil, attempt: 0) # :nodoc:
if attempt > @config[:max_attempts]
raise EBSCO::EDS::ApiError, 'EBSCO API error: Multiple attempts to perform request failed.'
end
begin
conn = connection
config_path = path.dup
# use a citation api connection?
if path.include?(@config[:citation_exports_url]) || path.include?(@config[:citation_styles_url])
conn = citation_connection
end
resp = conn.send(method) do |req|
case method
when :get
unless payload.nil?
qs = CGI.unescape(payload.to_query(nil))
config_path << '?' + qs
end
req.url config_path
when :post
unless payload.nil?
json_payload = JSON.generate(payload)
config_path << get_cache_id(path, json_payload) if @use_cache
req.body = json_payload
end
req.url config_path
else
raise EBSCO::EDS::ApiError, "EBSCO API error: Method #{method} not supported for endpoint #{config_path}"
end
end
resp.body
rescue Error => e
# try alternate EDS hosts
if e.is_a?(EBSCO::EDS::InternalServerError) || e.is_a?(EBSCO::EDS::ServiceUnavailable) || e.is_a?(EBSCO::EDS::ConnectionFailed)
if @api_hosts_list.length > @api_host_index+1
@api_host_index = @api_host_index+1
do_request(method, path: path, payload: payload, attempt: attempt+1)
else
raise EBSCO::EDS::ApiError, 'EBSCO API error: Unable to establish a connection to any EDS host.'
end
end
if e.respond_to? 'fault'
error_code = e.fault[:error_body]['ErrorNumber'] || e.fault[:error_body]['ErrorCode']
unless error_code.nil?
case error_code
# session token missing
when '108', '109'
@session_token = create_session_token
do_request(method, path: path, payload: payload, attempt: attempt+1)
# auth token invalid
when '104', '107'
@auth_token = nil
@auth_token = create_auth_token
do_request(method, path: path, payload: payload, attempt: attempt+1)
# trying to paginate in results list beyond 250 results
when '138'
is_jump_retry = false
is_orig_retry = false
# create a jump request payload
jump_payload = payload.clone
# retry failed jump requests (known API issue)
if jump_payload.instance_variable_defined?(:@Comment)
if jump_payload.Comment == 'jump_request'
is_jump_retry = true
puts '138 JUMP RETRY ================================================================' if @debug
do_jump_request(method, path: path, payload: jump_payload, attempt: attempt+1)
elsif jump_payload.Comment == 'jump_request_orig'
is_orig_retry = true
puts '138 ORIG RETRY ================================================================' if @debug
jump_response = do_jump_request(method, path: path, payload: payload, attempt: attempt+1)
if jump_response.success?
return jump_response.body
end
else
puts '138 ERROR =====================================================================' if @debug
end
end
# only perform these steps if it's the original 138 error
unless is_jump_retry or is_orig_retry
# remove these variables since they prevent a jump request (they continue to cause more 138 errors)
if jump_payload.SearchCriteria.instance_variable_defined?(:@AutoSuggest)
jump_payload.SearchCriteria.remove_instance_variable(:@AutoSuggest)
end
if jump_payload.SearchCriteria.instance_variable_defined?(:@AutoCorrect)
jump_payload.SearchCriteria.remove_instance_variable(:@AutoCorrect)
end
if jump_payload.SearchCriteria.instance_variable_defined?(:@Expanders)
jump_payload.SearchCriteria.remove_instance_variable(:@Expanders)
end
if jump_payload.SearchCriteria.instance_variable_defined?(:@RelatedContent)
jump_payload.SearchCriteria.remove_instance_variable(:@RelatedContent)
end
if jump_payload.SearchCriteria.instance_variable_defined?(:@Limiters)
jump_payload.SearchCriteria.remove_instance_variable(:@Limiters)
end
# get list of jump pages and make requests for each one before requesting the original request
jump_pages = get_jump_pages(payload)
# todo: truncate to @confi[:max_page_jumps]
jump_pages.each { |page|
jump_payload.Actions = ["GoToPage(#{page})"]
jump_payload.Comment = 'jump_request' # comment the request so we can retry if necessary
do_jump_request(method, path: path, payload: jump_payload, attempt: attempt+1)
}
# now make the original request (which can also require retries)
payload.Comment = 'jump_request_orig'
do_request(method, path: path, payload: payload, attempt: attempt+1)
end
# invalid source type, attempt to recover gracefully
when '130'
if @recover_130
bad_source_type = e.fault[:error_body]['DetailedErrorDescription']
bad_source_type.gsub!(/Value Provided\s+/, '')
bad_source_type.gsub!(/\.\s*$/, '')
new_actions = []
payload.Actions.each { |action|
if action.downcase.start_with?('addfacetfilter(sourcetype:')
if bad_source_type.nil?
# skip the source type since we don't know if it's bad or not
else
if !action.include?('SourceType:'+bad_source_type+')')
# not a bad source type, keep it
new_actions << action
end
end
else
# not a source type action, add it
new_actions << action
end
}
new_filters = []
filter_id = 1
payload.SearchCriteria.FacetFilters.each { |filter|
filter['FacetValues'].each { |facet_val|
if facet_val['Id'] == 'SourceType'
if bad_source_type.nil?
# skip the source type since we don't know if it's bad or not
else
# not a bad sourcetype, add it
if !facet_val['Value'].include?(bad_source_type)
filter['FilterId'] = filter_id
filter_id += 1
new_filters << filter
end
end
else
# not a SourceType filter, add it
filter['FilterId'] = filter_id
filter_id += 1
new_filters << filter
end
}
}
payload.SearchCriteria.FacetFilters = new_filters
payload.Actions = new_actions
do_request(method, path: path, payload: payload, attempt: attempt+1)
else
raise e
end
else
raise e
end
end
else
raise e
end
end
end
def do_jump_request(method, path:, payload: nil, attempt: 0) # :nodoc:
if attempt > @config[:max_page_jump_attempts]
raise EBSCO::EDS::ApiError, 'EBSCO API error: Multiple attempts to perform request failed.'
end
begin
if @debug
if payload.instance_variable_defined?(:@Actions)
puts 'JUMP ACTION: ' + payload.Actions.inspect if @debug
end
puts 'JUMP ATTEMPT: ' + attempt.to_s if @debug
end
# turn off caching
resp = jump_connection.send(method) do |req|
case method
when :get
req.url path
when :post
req.url path
unless payload.nil?
req.body = JSON.generate(payload)
end
else
raise EBSCO::EDS::ApiError, "EBSCO API error: Method #{method} not supported for endpoint #{path}"
end
end
resp
rescue Error => e
if e.respond_to? 'fault'
error_code = e.fault[:error_body]['ErrorNumber'] || e.fault[:error_body]['ErrorCode']
unless error_code.nil?
case error_code
when '138'
sleep Random.new.rand(1..3)
do_jump_request(method, path: path, payload: payload, attempt: attempt+1)
else
raise e
end
end
end
end
end
# --
# attempts to query profile capabilities
# dummy search just to get the list of available databases
# ++
def get_available_databases # :nodoc:
search({query: 'supercalifragilisticexpialidocious-supercalifragilisticexpialidocious',
results_per_page: 1,
mode: 'all',
include_facets: false}).database_stats
end
# :category: Profile Settings Methods
# Get a list of all available database IDs.
# Returns Array of IDs.
def get_available_database_ids
get_available_databases.map{|item| item[:id]}
end
# :category: Profile Settings Methods
# Determine if a database ID is available in the profile.
# Returns Boolean.
def dbid_in_profile(dbid)
get_available_database_ids.include? dbid
end
# :category: Profile Settings Methods
# Determine if publication matching is available in the profile.
# Returns Boolean.
def publication_match_in_profile
@info.available_related_content_types.include? 'emp'
end
# :category: Profile Settings Methods
# Determine if research starters are available in the profile.
# Returns Boolean.
def research_starters_match_in_profile
@info.available_related_content_types.include? 'rs'
end
private
def connection
logger = Logger.new(@config[:log])
logger.level = Logger.const_get(@log_level)
Faraday.new(url: 'https://' + @api_hosts_list[@api_host_index]) do |conn|
conn.headers['Content-Type'] = 'application/json;charset=UTF-8'
conn.headers['Accept'] = 'application/json'
conn.headers['x-sessionToken'] = @session_token ? @session_token : ''
conn.headers['x-authenticationToken'] = @auth_token ? @auth_token : ''
conn.headers['User-Agent'] = @config[:user_agent]
conn.request :url_encoded
conn.use :eds_caching_middleware, store: @cache_store, logger: @debug ? logger : nil if @use_cache
conn.use :eds_exception_middleware
conn.response :json, content_type: /\bjson$/
conn.response :detailed_logger, logger if @debug
conn.options[:open_timeout] = @config[:open_timeout]
conn.options[:timeout] = @config[:timeout]
conn.adapter :net_http_persistent
end
end
# same as above but no caching
def jump_connection
logger = Logger.new(@config[:log])
logger.level = Logger.const_get(@log_level)
Faraday.new(url: 'https://' + @api_hosts_list[@api_host_index]) do |conn|
conn.headers['Content-Type'] = 'application/json;charset=UTF-8'
conn.headers['Accept'] = 'application/json'
conn.headers['x-sessionToken'] = @session_token ? @session_token : ''
conn.headers['x-authenticationToken'] = @auth_token ? @auth_token : ''
conn.headers['User-Agent'] = @config[:user_agent]
conn.request :url_encoded
conn.use :eds_exception_middleware
conn.response :json, content_type: /\bjson$/
conn.response :detailed_logger, logger if @debug
conn.options[:open_timeout] = @config[:open_timeout]
conn.options[:timeout] = @config[:timeout]
conn.adapter :net_http_persistent
end
end
def citation_connection
logger = Logger.new(@config[:log])
logger.level = Logger.const_get(@log_level)
Faraday.new(url: 'https://' + @api_hosts_list[@api_host_index]) do |conn|
conn.headers['Content-Type'] = 'application/json;charset=UTF-8'
conn.headers['Accept'] = 'application/json'
conn.headers['x-sessionToken'] = @citation_token ? @citation_token : ''
conn.headers['x-authenticationToken'] = @auth_token ? @auth_token : ''
conn.headers['User-Agent'] = @config[:user_agent]
conn.request :url_encoded
conn.use :eds_caching_middleware, store: @cache_store, logger: @debug ? logger : nil if @use_cache
conn.use :eds_exception_middleware
conn.response :json, content_type: /\bjson$/
conn.response :detailed_logger, logger if @debug
conn.options[:open_timeout] = @config[:open_timeout]
conn.options[:timeout] = @config[:timeout]
conn.adapter :net_http_persistent
end
end
def create_auth_token
if blank?(@auth_token)
# ip auth
if (blank?(@user) && blank?(@pass)) || @auth_type.casecmp('ip').zero?
resp = do_request(:post, path: @config[:ip_auth_url])
# user auth
else
resp = do_request(:post, path: @config[:uid_auth_url], payload:
{ UserId: @user, Password: @pass, InterfaceId: @config[:interface_id] })
end
end
@auth_token = resp['AuthToken']
@auth_token
end
def create_session_token
guest_string = @guest ? 'y' : 'n'
resp = do_request(:get, path: @config[:create_session_url] +
'?profile=' + @profile + '&guest=' + guest_string +
'&displaydatabasename=y')
@session_token = resp['SessionToken']
end
def create_citation_token
resp = do_request(:get, path: @config[:create_session_url] +
'?profile=' + @profile + '&guest=n&displaydatabasename=y')
@citation_token = resp['SessionToken']
end
# helper methods
def blank?(var)
var.nil? || var.respond_to?(:length) && var.empty?
end
# used to reliably create empty results when there are no search terms provided
def empty_results(hits = 0)
{
'SearchRequest'=>
{
'SearchCriteria'=>
{
'Queries'=>nil,
'SearchMode'=>'',
'IncludeFacets'=>'y',
'Sort'=>'relevance',
'AutoSuggest'=>'n',
'AutoCorrect'=>'n'
},
'RetrievalCriteria'=>
{
'View'=>'brief',
'ResultsPerPage'=>20,
'Highlight'=>'y',
'IncludeImageQuickView'=>'n'
},
'SearchCriteriaWithActions'=>
{
'QueriesWithAction'=>nil
}
},
'SearchResult'=>
{
'Statistics'=>
{
'TotalHits'=>hits,
'TotalSearchTime'=>0,
'Databases'=>[]
},
'Data'=> {'RecordFormat'=>'EP Display'},
'AvailableCriteria'=>{'DateRange'=>{'MinDate'=>'0001-01', 'MaxDate'=>'0001-01'}}
}
}
end
# generate a cache id for search and retrieve post requests, using a hash of the payload + guest mode
def get_cache_id(path, payload)
if path == '/edsapi/rest/Search' or path == '/edsapi/rest/Retrieve'
'?cache_id=' + Digest::MD5.hexdigest(payload + @guest.to_s)
else
''
end
end
def eds_sanitize(str)
pattern = /([)(:,])/
str = str.gsub(pattern){ |match| '\\' + match }
str
end
def get_jump_pages(search_options)
dest_page = search_options.RetrievalCriteria.PageNumber.to_i
jump_incr = 250/search_options.RetrievalCriteria.ResultsPerPage.to_i
attempts = dest_page/jump_incr
jump_pages = []
(1..attempts).to_a.each do |n|
jump_pages.push(jump_incr*n)
end
puts 'JUMP PAGES: ' + jump_pages.inspect if @debug
jump_pages
end
end
end
end