bento_search 0.0.1
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.
- data/MIT-LICENSE +20 -0
- data/README.md +299 -0
- data/Rakefile +40 -0
- data/app/assets/images/bento_search/large_loader.gif +0 -0
- data/app/assets/javascripts/bento_search.js +3 -0
- data/app/assets/javascripts/bento_search/ajax_load.js +22 -0
- data/app/assets/stylesheets/bento_search/bento.css +4 -0
- data/app/controllers/bento_search/bento_search_controller.rb +7 -0
- data/app/controllers/bento_search/search_controller.rb +72 -0
- data/app/helpers/bento_search_helper.rb +138 -0
- data/app/item_decorators/bento_search/only_premade_openurl.rb +16 -0
- data/app/item_decorators/bento_search/openurl_add_other_link.rb +35 -0
- data/app/item_decorators/bento_search/openurl_main_link.rb +30 -0
- data/app/models/bento_search/author.rb +25 -0
- data/app/models/bento_search/link.rb +30 -0
- data/app/models/bento_search/multi_searcher.rb +109 -0
- data/app/models/bento_search/openurl_creator.rb +128 -0
- data/app/models/bento_search/registrar.rb +70 -0
- data/app/models/bento_search/result_item.rb +203 -0
- data/app/models/bento_search/results.rb +54 -0
- data/app/models/bento_search/results/pagination.rb +67 -0
- data/app/models/bento_search/search_engine.rb +219 -0
- data/app/models/bento_search/search_engine/capabilities.rb +65 -0
- data/app/search_engines/bento_search/#Untitled-1# +11 -0
- data/app/search_engines/bento_search/ebsco_host_engine.rb +356 -0
- data/app/search_engines/bento_search/eds_engine.rb +557 -0
- data/app/search_engines/bento_search/google_books_engine.rb +184 -0
- data/app/search_engines/bento_search/primo_engine.rb +231 -0
- data/app/search_engines/bento_search/scopus_engine.rb +295 -0
- data/app/search_engines/bento_search/summon_engine.rb +398 -0
- data/app/search_engines/bento_search/xerxes_engine.rb +168 -0
- data/app/views/bento_search/_link.html.erb +4 -0
- data/app/views/bento_search/_search_error.html.erb +22 -0
- data/app/views/bento_search/_std_item.html.erb +39 -0
- data/app/views/bento_search/search/search.html.erb +1 -0
- data/config/locales/en.yml +25 -0
- data/lib/bento_search.rb +29 -0
- data/lib/bento_search/engine.rb +5 -0
- data/lib/bento_search/routes.rb +45 -0
- data/lib/bento_search/version.rb +3 -0
- data/lib/generators/bento_search/pull_ebsco_dbs_generator.rb +24 -0
- data/lib/generators/bento_search/templates/ebsco_global_var.erb +6 -0
- data/lib/http_client_patch/include_client.rb +86 -0
- data/lib/tasks/bento_search_tasks.rake +4 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +56 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +3100 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/functional/bento_search/search_controller_test.rb +81 -0
- data/test/helper/bento_search_helper_test.rb +125 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/support/mock_engine.rb +23 -0
- data/test/support/test_with_cassette.rb +38 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/#vcr_test.rb# +68 -0
- data/test/unit/ebsco_host_engine_test.rb +134 -0
- data/test/unit/eds_engine_test.rb +105 -0
- data/test/unit/google_books_engine_test.rb +93 -0
- data/test/unit/item_decorators_test.rb +66 -0
- data/test/unit/multi_searcher_test.rb +49 -0
- data/test/unit/openurl_creator_test.rb +111 -0
- data/test/unit/pagination_test.rb +59 -0
- data/test/unit/primo_engine_test.rb +37 -0
- data/test/unit/register_engine_test.rb +50 -0
- data/test/unit/result_item_display_test.rb +39 -0
- data/test/unit/result_item_test.rb +36 -0
- data/test/unit/scopus_engine_test.rb +130 -0
- data/test/unit/search_engine_base_test.rb +178 -0
- data/test/unit/search_engine_test.rb +95 -0
- data/test/unit/summon_engine_test.rb +161 -0
- data/test/unit/xerxes_engine_test.rb +70 -0
- data/test/vcr_cassettes/ebscohost/error_bad_db.yml +45 -0
- data/test/vcr_cassettes/ebscohost/error_bad_password.yml +45 -0
- data/test/vcr_cassettes/ebscohost/get_info.yml +3626 -0
- data/test/vcr_cassettes/ebscohost/live_search.yml +45 -0
- data/test/vcr_cassettes/ebscohost/live_search_smoke_test.yml +1311 -0
- data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1811 -0
- data/test/vcr_cassettes/eds/get_auth_token.yml +75 -0
- data/test/vcr_cassettes/eds/get_auth_token_failure.yml +39 -0
- data/test/vcr_cassettes/eds/get_with_auth.yml +243 -0
- data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +368 -0
- data/test/vcr_cassettes/gbs/error_condition.yml +40 -0
- data/test/vcr_cassettes/gbs/pagination.yml +702 -0
- data/test/vcr_cassettes/gbs/search.yml +340 -0
- data/test/vcr_cassettes/primo/search_smoke_test.yml +1112 -0
- data/test/vcr_cassettes/scopus/bad_api_key_should_return_error_response.yml +60 -0
- data/test/vcr_cassettes/scopus/escaped_chars.yml +187 -0
- data/test/vcr_cassettes/scopus/fielded_search.yml +176 -0
- data/test/vcr_cassettes/scopus/simple_search.yml +227 -0
- data/test/vcr_cassettes/scopus/zero_results_search.yml +67 -0
- data/test/vcr_cassettes/summon/bad_auth.yml +54 -0
- data/test/vcr_cassettes/summon/proper_tags_for_snippets.yml +216 -0
- data/test/vcr_cassettes/summon/search.yml +242 -0
- data/test/vcr_cassettes/xerxes/live_search.yml +2580 -0
- data/test/view/std_item_test.rb +98 -0
- metadata +421 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Rails helper module provided by BentoSearch meant to be included
|
|
2
|
+
# in host app's helpers.
|
|
3
|
+
module BentoSearchHelper
|
|
4
|
+
|
|
5
|
+
# Renders bento search results on page, or an AJAX loader, etc, as appropriate.
|
|
6
|
+
# Pass in:
|
|
7
|
+
# * BentoSearch::SearchResults => will render
|
|
8
|
+
# * an instantiated BentoSearch::SearchEngine => Will do search and render
|
|
9
|
+
# * an id that a search engine was registered under with
|
|
10
|
+
# BentoSearch.register_engine => will do search and render.
|
|
11
|
+
#
|
|
12
|
+
# Second arg options hash includes options for bento_search helper,
|
|
13
|
+
# as well as other options pased on to BentoSearch::Engine.search(options)
|
|
14
|
+
#
|
|
15
|
+
# == Options
|
|
16
|
+
#
|
|
17
|
+
# load: :ajax_auto, :immediate. :ajax_auto will put a spinner up there,
|
|
18
|
+
# and load actual results via AJAX request. :immediate preloads
|
|
19
|
+
# results.
|
|
20
|
+
#
|
|
21
|
+
# == Examples
|
|
22
|
+
#
|
|
23
|
+
# bento_results( results_obj )
|
|
24
|
+
# bento_results( engine_obj, :query => "cancer")
|
|
25
|
+
# bento_results("google_books", :query => "cancer", :load => :ajax_auto)
|
|
26
|
+
#
|
|
27
|
+
def bento_search(search_arg, options = {})
|
|
28
|
+
results = search_arg if search_arg.kind_of? BentoSearch::Results
|
|
29
|
+
|
|
30
|
+
load_mode = options.delete(:load)
|
|
31
|
+
|
|
32
|
+
engine = nil
|
|
33
|
+
unless results
|
|
34
|
+
# need to load an engine and do a search, or ajax, etc.
|
|
35
|
+
engine = (if search_arg.kind_of? BentoSearch::SearchEngine
|
|
36
|
+
search_arg
|
|
37
|
+
else
|
|
38
|
+
BentoSearch.get_engine(search_arg.to_s)
|
|
39
|
+
end)
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if (!results && load_mode == :ajax_auto)
|
|
44
|
+
raise ArgumentError.new("`:load => :ajax` requires a registered engine with an id") unless engine.configuration.id
|
|
45
|
+
content_tag(:div, :class => "bento_search_ajax_wait",
|
|
46
|
+
:"data-bento-ajax-url" => to_bento_search_url( {:engine_id => engine.configuration.id}.merge(options) )) do
|
|
47
|
+
image_tag("bento_search/large_loader.gif", :alt => I18n.translate("bento_search.ajax_loading")) +
|
|
48
|
+
content_tag("noscript") do
|
|
49
|
+
"Can not load results without javascript"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
results = engine.search(options) unless results
|
|
54
|
+
|
|
55
|
+
if results.failed?
|
|
56
|
+
render :partial => "bento_search/search_error", :locals => {:results => results}
|
|
57
|
+
elsif results.length > 0
|
|
58
|
+
render :partial => "bento_search/std_item", :collection => results
|
|
59
|
+
else
|
|
60
|
+
content_tag(:div, :class=> "bento_search_no_results") do
|
|
61
|
+
I18n.translate("bento_search.no_results")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# More methods used by bento standard views, namespaced with bento_, sorry
|
|
70
|
+
# no great way to take logic out of views into helper methods without
|
|
71
|
+
# namespacey hack.
|
|
72
|
+
#
|
|
73
|
+
# You can use these methods in your own custom views, you also should be
|
|
74
|
+
# able to over-ride them (including calling super) in local helpers to
|
|
75
|
+
# change behavior in standard views.
|
|
76
|
+
#
|
|
77
|
+
##
|
|
78
|
+
|
|
79
|
+
def bento_abstract_truncate(str)
|
|
80
|
+
# if it's html safe, we can't truncate it, we don't have an HTML-aware
|
|
81
|
+
# truncation routine right now, that avoids leaving tags open etc.
|
|
82
|
+
return str if str.html_safe?
|
|
83
|
+
|
|
84
|
+
truncate(str, :length => 280, :separator => " ")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Prepare a title in an H4, with formats in parens in a <small> (for
|
|
88
|
+
# bootstrap), linked, etc.
|
|
89
|
+
def bento_item_title(item)
|
|
90
|
+
content_tag("h4", :class => "bento_item_title") do
|
|
91
|
+
safe_join([
|
|
92
|
+
link_to_unless( item.link.blank?, item.complete_title, item.link ),
|
|
93
|
+
if item.format.present? || item.format_str.present?
|
|
94
|
+
content_tag("small", :class => "bento_item_about") do
|
|
95
|
+
" (" +
|
|
96
|
+
if item.format_str
|
|
97
|
+
item.format_str
|
|
98
|
+
else
|
|
99
|
+
t(item.format, :scope => [:bento_search, :format], :default => item.format.to_s.titleize)
|
|
100
|
+
end + ")"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
], '')
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# first 3 authors, each in a <span>, using item.author_display, seperated by
|
|
108
|
+
# semi-colons.
|
|
109
|
+
def bento_item_authors(item)
|
|
110
|
+
parts = []
|
|
111
|
+
|
|
112
|
+
first_three = item.authors.slice(0,3)
|
|
113
|
+
|
|
114
|
+
first_three.each_with_index do |author, index|
|
|
115
|
+
parts << content_tag("span", :class => "bento_item_author") do
|
|
116
|
+
item.author_display(author)
|
|
117
|
+
end
|
|
118
|
+
if (index + 1) < first_three.length
|
|
119
|
+
parts << "; "
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return safe_join(parts, "")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# returns a hash of label => key suitable for passing to rails
|
|
127
|
+
# options_for_select. (Yes, it works backwards from how you'd expect).
|
|
128
|
+
# Label is looked up using I18n, at bento_search.sort_keys.*
|
|
129
|
+
def bento_sort_hash_for(engine)
|
|
130
|
+
Hash[
|
|
131
|
+
engine.sort_definitions.keys.collect do |key|
|
|
132
|
+
[I18n.translate(key, :scope => "bento_search.sort_keys"), key]
|
|
133
|
+
end
|
|
134
|
+
]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'openurl'
|
|
2
|
+
|
|
3
|
+
# A Decorator that will make #to_openurl refuse to construct
|
|
4
|
+
# an openurl from individual elements, it'll use the #openurl_kev_co
|
|
5
|
+
# or nothing.
|
|
6
|
+
module BentoSearch::OnlyPremadeOpenurl
|
|
7
|
+
|
|
8
|
+
def to_openurl
|
|
9
|
+
if self.openurl_kev_co
|
|
10
|
+
return OpenURL::ContextObject.new_from_kev( self.openurl_kev_co )
|
|
11
|
+
else
|
|
12
|
+
return nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Example of an Item Decorator that ADDs an 'other link' with an openurl.
|
|
2
|
+
#
|
|
3
|
+
# This example uses crazy metaprogramming to dynamically create
|
|
4
|
+
# a module configured with your base url etc. You don't need to use
|
|
5
|
+
# crazy method like that; just define your own local decorator doing
|
|
6
|
+
# exactly what you need, it's meant to be simple.
|
|
7
|
+
#
|
|
8
|
+
# config.item_decorators = [ BentoSearch::OpenurlAddOtherLink[:base_url => "http://resolve.somewhere.edu/foo", :extra_query => "&foo=bar"] ]
|
|
9
|
+
#
|
|
10
|
+
module BentoSearch::OpenurlAddOtherLink
|
|
11
|
+
def self.[](options)
|
|
12
|
+
base_url = options[:base_url]
|
|
13
|
+
extra_query = options[:extra_query] || ""
|
|
14
|
+
link_name = options[:link_name] || "Find It"
|
|
15
|
+
# overwrite: if true, overwrite previously existing other_links, if
|
|
16
|
+
# false add on to previously existing.
|
|
17
|
+
overwrite = options.has_key?(:overwrite) ? options[:overwrite] : false
|
|
18
|
+
|
|
19
|
+
Module.new do
|
|
20
|
+
|
|
21
|
+
define_method :other_links do
|
|
22
|
+
start = overwrite ? [] : super()
|
|
23
|
+
if (ou = to_openurl)
|
|
24
|
+
start + [BentoSearch::Link.new(:url => "#{base_url}?#{ou.kev}#{extra_query}", :label => link_name)]
|
|
25
|
+
else
|
|
26
|
+
start
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Example of an Item Decorator that replaces the main 'link'
|
|
2
|
+
# with an openurl.
|
|
3
|
+
#
|
|
4
|
+
# This example uses crazy metaprogramming to dynamically create
|
|
5
|
+
# a module configured with your base url etc. You don't need to use
|
|
6
|
+
# crazy method like that; just define your own local decorator doing
|
|
7
|
+
# exactly what you need, it's meant to be simple.
|
|
8
|
+
#
|
|
9
|
+
# config.item_decorators = [ BentoSearch::OpenurlMainLink[:base_url => "http://resolve.somewhere.edu/foo", :extra_query => "&foo=bar"] ]
|
|
10
|
+
#
|
|
11
|
+
module BentoSearch::OpenurlMainLink
|
|
12
|
+
def self.[](options)
|
|
13
|
+
base_url = options[:base_url]
|
|
14
|
+
extra_query = options[:extra_query] || ""
|
|
15
|
+
Module.new do
|
|
16
|
+
|
|
17
|
+
define_method :link do
|
|
18
|
+
if (ou = to_openurl)
|
|
19
|
+
"#{base_url}?#{ou.kev}#{extra_query}"
|
|
20
|
+
else
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module BentoSearch
|
|
2
|
+
class Author
|
|
3
|
+
def initialize(args ={})
|
|
4
|
+
args.each_pair do |key, value|
|
|
5
|
+
send("#{key}=", value)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Can be first name or initial, whatever source provides
|
|
10
|
+
attr_accessor :first
|
|
11
|
+
# last name/surname/family name as provided by source
|
|
12
|
+
attr_accessor :last
|
|
13
|
+
# middle name or initial, as and if provided by source
|
|
14
|
+
attr_accessor :middle
|
|
15
|
+
|
|
16
|
+
# if source doens't provide seperate first/last,
|
|
17
|
+
# source may only be able to provide one big string, author_display
|
|
18
|
+
attr_accessor :display
|
|
19
|
+
|
|
20
|
+
def empty?
|
|
21
|
+
first.blank? && last.blank? && middle.blank? && display.blank?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module BentoSearch
|
|
2
|
+
# Represents an 'additional link' held in BentoSearch::ResultItem#other_links
|
|
3
|
+
#
|
|
4
|
+
# label, url, other metadata about the link.
|
|
5
|
+
class Link
|
|
6
|
+
# url is normally a string, but can also be a Hash passed
|
|
7
|
+
# to url_for in the local app.
|
|
8
|
+
attr_accessor :label, :url
|
|
9
|
+
|
|
10
|
+
# Used both for HTML links and possibly later Atom.
|
|
11
|
+
# Must be a string, EITHER a complete URL (representing
|
|
12
|
+
# vocab term), OR a legal short name from
|
|
13
|
+
# http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#linkTypes
|
|
14
|
+
attr_accessor :rel
|
|
15
|
+
|
|
16
|
+
# Array of strings, used for CSS classes on this link, possibly
|
|
17
|
+
# for custom styles/images etc. May be used in non-html link
|
|
18
|
+
# contexts too.
|
|
19
|
+
attr_accessor :style_classes
|
|
20
|
+
|
|
21
|
+
def initialize(hash = {})
|
|
22
|
+
self.style_classes = []
|
|
23
|
+
|
|
24
|
+
hash.each_pair do |key, value|
|
|
25
|
+
send("#{key}=", value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'celluloid'
|
|
2
|
+
|
|
3
|
+
# Based on Celluloid, concurrently runs multiple searches in
|
|
4
|
+
# seperate threads. You must include 'celluloid' gem dependency
|
|
5
|
+
# into your local app to use this class.
|
|
6
|
+
#
|
|
7
|
+
# I am not an expert at use of Celluloid, it's possible there's a better
|
|
8
|
+
# way to do this all, but seems to work.
|
|
9
|
+
#
|
|
10
|
+
# ## Usage
|
|
11
|
+
#
|
|
12
|
+
# initialize with id's of registered engines:
|
|
13
|
+
# searcher = BentoBox::MultiSearcher.new(:gbs, :scopus)
|
|
14
|
+
#
|
|
15
|
+
# start the concurrent searches, params same as engine.search
|
|
16
|
+
# searcher.start( query_params )
|
|
17
|
+
#
|
|
18
|
+
# retrieve results, blocking until each is completed:
|
|
19
|
+
# searcher.results
|
|
20
|
+
#
|
|
21
|
+
# returns a Hash keyed by engine id, values BentoSearch::Results objects.
|
|
22
|
+
#
|
|
23
|
+
# Can only call #results once per #start, after that it'll return empty hash.
|
|
24
|
+
# (should we make it actually raise instead?). .
|
|
25
|
+
#
|
|
26
|
+
# important to call results at some point after calling start, in order
|
|
27
|
+
# to make sure Celluloid::Actors are properly terminated to avoid
|
|
28
|
+
# resource leakage.
|
|
29
|
+
#
|
|
30
|
+
# TODO: have a method that returns Futures instead of only supplying the blocking
|
|
31
|
+
# results method? Several tricks, including making sure to properly terminate actors.
|
|
32
|
+
class BentoSearch::MultiSearcher
|
|
33
|
+
|
|
34
|
+
def initialize(*engine_ids)
|
|
35
|
+
@engines = []
|
|
36
|
+
@actors = []
|
|
37
|
+
engine_ids.each do |id|
|
|
38
|
+
add_engine( BentoSearch.get_engine id )
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Adds an instantiated engine directly, rather than by id from global
|
|
43
|
+
# registry.
|
|
44
|
+
def add_engine(engine)
|
|
45
|
+
@engines << engine
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Starts all searches, returns self so you can chain method calls if you like.
|
|
49
|
+
def start(*search_args)
|
|
50
|
+
@engines.each do |engine|
|
|
51
|
+
a = Actor.new(engine)
|
|
52
|
+
@actors << a
|
|
53
|
+
a.start! *search_args
|
|
54
|
+
end
|
|
55
|
+
return self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Call after #start. Blocks until each included engine is finished
|
|
59
|
+
# then returns a Hash keyed by engine registered id, value is a
|
|
60
|
+
# BentoSearch::Results object.
|
|
61
|
+
#
|
|
62
|
+
# Can only call _once_ per invocation of #start, after that it'll return
|
|
63
|
+
# an empty hash.
|
|
64
|
+
def results
|
|
65
|
+
results = {}
|
|
66
|
+
|
|
67
|
+
# we use #delete_if to get an iterator that deletes
|
|
68
|
+
# each item after iteration.
|
|
69
|
+
@actors.delete_if do |actor|
|
|
70
|
+
result_key = (actor.engine.configuration.id || actor.engine.class.name)
|
|
71
|
+
results[result_key] = actor.results
|
|
72
|
+
actor.terminate
|
|
73
|
+
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return results
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Actor
|
|
82
|
+
include Celluloid
|
|
83
|
+
|
|
84
|
+
attr_accessor :engine
|
|
85
|
+
|
|
86
|
+
def initialize(a_engine)
|
|
87
|
+
self.engine = a_engine
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# call as start! with bang, to invoke async.
|
|
91
|
+
def start(*search_args)
|
|
92
|
+
begin
|
|
93
|
+
@results = self.engine.search(*search_args)
|
|
94
|
+
rescue Exception => e
|
|
95
|
+
warn e
|
|
96
|
+
# Make a fake results with caught exception.
|
|
97
|
+
@results = BentoSearch::Results.new
|
|
98
|
+
@results.error ||= {}
|
|
99
|
+
@results.error["exception"] = e
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def results
|
|
104
|
+
@results
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require 'openurl'
|
|
2
|
+
|
|
3
|
+
module BentoSearch
|
|
4
|
+
|
|
5
|
+
# Helper class used to take a ResultItem, and construct
|
|
6
|
+
# a ruby OpenURL::ContextObject out of it. That represents
|
|
7
|
+
# a NISO Z39.88 OpenURL context object, useful for using
|
|
8
|
+
# with linking software that expects such. http://en.wikipedia.org/wiki/OpenURL
|
|
9
|
+
#
|
|
10
|
+
# co = OpenurlCreator.new( result_item ).to_open_url
|
|
11
|
+
# # => ruby OpenURL::ContextObject object.
|
|
12
|
+
#
|
|
13
|
+
# co.kev
|
|
14
|
+
# # => context object serialized to KEV format (URL query string)
|
|
15
|
+
#
|
|
16
|
+
# In some cases nil can be returned, if no reasonable OpenURL can
|
|
17
|
+
# be created from the ResultItem.
|
|
18
|
+
class OpenurlCreator
|
|
19
|
+
attr_accessor :result_item
|
|
20
|
+
|
|
21
|
+
def initialize(ri)
|
|
22
|
+
self.result_item = ri
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_openurl
|
|
26
|
+
# If we have a pre-constructed KEV, just use it.
|
|
27
|
+
if result_item.openurl_kev_co
|
|
28
|
+
return OpenURL::ContextObject.new_from_kev( result_item.openurl_kev_co )
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
context_object = OpenURL::ContextObject.new
|
|
33
|
+
|
|
34
|
+
r = context_object.referent
|
|
35
|
+
|
|
36
|
+
r.set_format( self.format )
|
|
37
|
+
|
|
38
|
+
if result_item.doi
|
|
39
|
+
r.add_identifier("info:doi:#{result_item.doi}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
r.set_metadata("genre", self.genre)
|
|
43
|
+
|
|
44
|
+
if result_item.authors.length > 0
|
|
45
|
+
r.set_metadata("aufirst", result_item.authors.first.first)
|
|
46
|
+
r.set_metadata("aulast", result_item.authors.first.last)
|
|
47
|
+
r.set_metadata("au", result_item.author_display(result_item.authors.first))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
r.set_metadata("date", result_item.year.to_s)
|
|
51
|
+
r.set_metadata("volume", result_item.volume.to_s)
|
|
52
|
+
r.set_metadata("issue", result_item.issue.to_s)
|
|
53
|
+
r.set_metadata("spage", result_item.start_page.to_s)
|
|
54
|
+
r.set_metadata("epage", result_item.end_page.to_s)
|
|
55
|
+
r.set_metadata("jtitle", result_item.journal_title)
|
|
56
|
+
r.set_metadata("issn", result_item.issn)
|
|
57
|
+
r.set_metadata("isbn", result_item.isbn)
|
|
58
|
+
r.set_metadata("pub", result_item.publisher)
|
|
59
|
+
|
|
60
|
+
case result_item.format
|
|
61
|
+
when "Book"
|
|
62
|
+
r.set_metadata("btitle", result_item.complete_title)
|
|
63
|
+
when "Article", :conference_paper
|
|
64
|
+
r.set_metadata("atitle", result_item.complete_title)
|
|
65
|
+
else
|
|
66
|
+
r.set_metadata("title", result_item.complete_title)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return context_object
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# rft.genre value. Yeah, the legal ones differ depending on openurl
|
|
74
|
+
# 'format', but we've given up trying to do things strictly legal,
|
|
75
|
+
# OpenURL is a bear, we do things as generally used and good enough.
|
|
76
|
+
#
|
|
77
|
+
# can be nil.
|
|
78
|
+
def genre
|
|
79
|
+
case result_item.format
|
|
80
|
+
when "Book"
|
|
81
|
+
"book"
|
|
82
|
+
when :book_item
|
|
83
|
+
"bookitem"
|
|
84
|
+
when :conference_paper
|
|
85
|
+
"proceeding"
|
|
86
|
+
when :conference_proceedings
|
|
87
|
+
"conference"
|
|
88
|
+
when :report
|
|
89
|
+
"report"
|
|
90
|
+
when :serial
|
|
91
|
+
"journal"
|
|
92
|
+
when "Article"
|
|
93
|
+
"article"
|
|
94
|
+
else
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# We need to map from our formats to which OpenURL 'format'
|
|
101
|
+
# we're going to create.
|
|
102
|
+
#
|
|
103
|
+
# We only pick from a limited set of standard scholarly citation formats,
|
|
104
|
+
# that's all any actual widespread software recognizes.
|
|
105
|
+
#
|
|
106
|
+
# Returns the last component of a valid format from:
|
|
107
|
+
# http://alcme.oclc.org/openurl/servlet/OAIHandler?verb=ListRecords&metadataPrefix=oai_dc&set=Core:Metadata+Formats
|
|
108
|
+
#
|
|
109
|
+
# Eg, "book", "journal", "dissertation".
|
|
110
|
+
#
|
|
111
|
+
# In fact, we only pick from one of those three -- by default, if we
|
|
112
|
+
# can't figure out exactly what it is or can't map it to a specific
|
|
113
|
+
# format, we'll return 'journal', never nil. 'journal' serves, in practice,
|
|
114
|
+
# with much actual software, as a neutral default.
|
|
115
|
+
def format
|
|
116
|
+
case result_item.format
|
|
117
|
+
when "Book"
|
|
118
|
+
"book"
|
|
119
|
+
when :dissertation
|
|
120
|
+
"dissertation"
|
|
121
|
+
else
|
|
122
|
+
"journal"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
end
|