umlaut 3.0.0alpha1

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.
Files changed (293) hide show
  1. data/LICENSE +7 -0
  2. data/README.md +49 -0
  3. data/Rakefile +37 -0
  4. data/app/assets/images/error.gif +0 -0
  5. data/app/assets/images/export_bg_bot.gif +0 -0
  6. data/app/assets/images/export_bg_mid.gif +0 -0
  7. data/app/assets/images/export_bg_top.gif +0 -0
  8. data/app/assets/images/famfamfam/book_open.png +0 -0
  9. data/app/assets/images/famfamfam/cross.png +0 -0
  10. data/app/assets/images/famfamfam/page_sound.gif +0 -0
  11. data/app/assets/images/famfamfam/page_text.gif +0 -0
  12. data/app/assets/images/famfamfam/page_up.gif +0 -0
  13. data/app/assets/images/famfamfam/page_white.png +0 -0
  14. data/app/assets/images/famfamfam/readme.html +1495 -0
  15. data/app/assets/images/famfamfam/tiny_cross.png +0 -0
  16. data/app/assets/images/frame_remove.gif +0 -0
  17. data/app/assets/images/ico_go.gif +0 -0
  18. data/app/assets/images/jhu_findit.gif +0 -0
  19. data/app/assets/images/list_closed.png +0 -0
  20. data/app/assets/images/list_open.png +0 -0
  21. data/app/assets/images/more_info.gif +0 -0
  22. data/app/assets/images/rails.png +0 -0
  23. data/app/assets/images/request.gif +0 -0
  24. data/app/assets/images/spinner.gif +0 -0
  25. data/app/assets/javascripts/umlaut/ajax_windows.js +35 -0
  26. data/app/assets/javascripts/umlaut/ensure_window_size.js.erb +34 -0
  27. data/app/assets/javascripts/umlaut/expand_contract_toggle.js +25 -0
  28. data/app/assets/javascripts/umlaut/search_autocomplete.js +46 -0
  29. data/app/assets/javascripts/umlaut/simple_visible_toggle.js +8 -0
  30. data/app/assets/javascripts/umlaut/update_html.js +152 -0
  31. data/app/assets/javascripts/umlaut.js +17 -0
  32. data/app/assets/stylesheets/umlaut.css +857 -0
  33. data/app/controllers/application_controller.rb +14 -0
  34. data/app/controllers/export_email_controller.rb +123 -0
  35. data/app/controllers/js_helper_controller.rb +10 -0
  36. data/app/controllers/link_router_controller.rb +87 -0
  37. data/app/controllers/open_search_controller.rb +9 -0
  38. data/app/controllers/resolve_controller.rb +288 -0
  39. data/app/controllers/resource_controller.rb +83 -0
  40. data/app/controllers/search_controller.rb +328 -0
  41. data/app/controllers/search_methods/sfx3.rb +148 -0
  42. data/app/controllers/search_methods/sfx4.rb +257 -0
  43. data/app/controllers/search_methods/sfx_api.rb +47 -0
  44. data/app/controllers/store_controller.rb +64 -0
  45. data/app/controllers/umlaut/controller_behavior.rb +20 -0
  46. data/app/controllers/umlaut/controller_logic.rb +96 -0
  47. data/app/controllers/umlaut/error_handling.rb +48 -0
  48. data/app/controllers/umlaut_controller.rb +112 -0
  49. data/app/helpers/application_helper.rb +4 -0
  50. data/app/helpers/emailer_helper.rb +43 -0
  51. data/app/helpers/export_email_helper.rb +34 -0
  52. data/app/helpers/open_search_helper.rb +7 -0
  53. data/app/helpers/resolve_helper.rb +225 -0
  54. data/app/helpers/search_helper.rb +50 -0
  55. data/app/helpers/umlaut/footer_helper.rb +64 -0
  56. data/app/helpers/umlaut/helper.rb +62 -0
  57. data/app/helpers/umlaut/html_head_helper.rb +37 -0
  58. data/app/helpers/umlaut/url_generation.rb +77 -0
  59. data/app/mailers/emailer.rb +48 -0
  60. data/app/models/clickthrough.rb +2 -0
  61. data/app/models/collection.rb +259 -0
  62. data/app/models/crossref_lookup.rb +2 -0
  63. data/app/models/dispatched_service.rb +58 -0
  64. data/app/models/permalink.rb +29 -0
  65. data/app/models/referent.rb +473 -0
  66. data/app/models/referent_value.rb +14 -0
  67. data/app/models/request.rb +449 -0
  68. data/app/models/service_response.rb +179 -0
  69. data/app/models/service_store.rb +59 -0
  70. data/app/models/service_type_value.rb +58 -0
  71. data/app/models/service_wave.rb +150 -0
  72. data/app/models/sfx_db/az_additional_title.rb +11 -0
  73. data/app/models/sfx_db/az_letter_group.rb +11 -0
  74. data/app/models/sfx_db/az_title.rb +38 -0
  75. data/app/models/sfx_db/az_title_v2.rb +34 -0
  76. data/app/models/sfx_db/isbn.rb +12 -0
  77. data/app/models/sfx_db/issn.rb +12 -0
  78. data/app/models/sfx_db/object.rb +35 -0
  79. data/app/models/sfx_db/object_portfolio.rb +6 -0
  80. data/app/models/sfx_db/publisher.rb +10 -0
  81. data/app/models/sfx_db/sfx_db_base.rb +54 -0
  82. data/app/models/sfx_db/target.rb +9 -0
  83. data/app/models/sfx_db/target_service.rb +10 -0
  84. data/app/models/sfx_db/title.rb +10 -0
  85. data/app/models/sfx_db.rb +10 -0
  86. data/app/models/sfx_url.rb +35 -0
  87. data/app/views/emailer/citation.text.erb +28 -0
  88. data/app/views/emailer/short_citation.text.erb +8 -0
  89. data/app/views/export_email/_email.html.erb +25 -0
  90. data/app/views/export_email/_send_email.html.erb +3 -0
  91. data/app/views/export_email/_send_txt.html.erb +3 -0
  92. data/app/views/export_email/_txt.html.erb +62 -0
  93. data/app/views/export_email/email.html.erb +3 -0
  94. data/app/views/export_email/send_email.html.erb +1 -0
  95. data/app/views/export_email/send_txt.html.erb +1 -0
  96. data/app/views/export_email/txt.html.erb +3 -0
  97. data/app/views/js_helper/loader.erb.js +13 -0
  98. data/app/views/layouts/umlaut.html.erb +52 -0
  99. data/app/views/open_search/index.html.erb +9 -0
  100. data/app/views/resolve/_api_in_progress.xml.erb +21 -0
  101. data/app/views/resolve/_background_progress.html.erb +51 -0
  102. data/app/views/resolve/_background_updater.html.erb +38 -0
  103. data/app/views/resolve/_citation.html.erb +87 -0
  104. data/app/views/resolve/_coins.html.erb +1 -0
  105. data/app/views/resolve/_compact_citation.html.erb +33 -0
  106. data/app/views/resolve/_cover_image.html.erb +35 -0
  107. data/app/views/resolve/_fulltext.html.erb +55 -0
  108. data/app/views/resolve/_help.html.erb +17 -0
  109. data/app/views/resolve/_holding.html.erb +91 -0
  110. data/app/views/resolve/_related_items.html.erb +35 -0
  111. data/app/views/resolve/_search_inside.html.erb +62 -0
  112. data/app/views/resolve/_section_display.html.erb +49 -0
  113. data/app/views/resolve/_service_errors.html.erb +29 -0
  114. data/app/views/resolve/_standard_response_item.html.erb +89 -0
  115. data/app/views/resolve/api.xml.builder +72 -0
  116. data/app/views/resolve/background_status.html.erb +26 -0
  117. data/app/views/resolve/index.html.erb +73 -0
  118. data/app/views/resolve/partial_html_sections.xml.erb +30 -0
  119. data/app/views/search/_a_to_z.html.erb +6 -0
  120. data/app/views/search/_citation.html.erb +94 -0
  121. data/app/views/search/_pager.html.erb +60 -0
  122. data/app/views/search/books.html.erb +103 -0
  123. data/app/views/search/journal_search.html.erb +90 -0
  124. data/app/views/search/journals.html.erb +167 -0
  125. data/app/views/search/opensearch_description.rxml +10 -0
  126. data/app/views/testing/index.html.erb +1 -0
  127. data/app/views/umlaut/README +5 -0
  128. data/app/views/umlaut/error.html.erb +45 -0
  129. data/db/migrate/01_umlaut_init.rb +113 -0
  130. data/db/orig_fixed_data/service_type_values.yml +120 -0
  131. data/db/seeds.rb +7 -0
  132. data/lib/CronTab.rb +192 -0
  133. data/lib/aws_product_sign.rb +146 -0
  134. data/lib/exlibris/aleph/patron.rb +64 -0
  135. data/lib/exlibris/aleph/record.rb +54 -0
  136. data/lib/exlibris/aleph/rest_api.rb +29 -0
  137. data/lib/exlibris/primo/holding.rb +192 -0
  138. data/lib/exlibris/primo/rsrc.rb +17 -0
  139. data/lib/exlibris/primo/searcher.rb +276 -0
  140. data/lib/exlibris/primo/source/aleph.rb +46 -0
  141. data/lib/exlibris/primo/source/distribution/nyu_aleph.rb +323 -0
  142. data/lib/exlibris/primo/toc.rb +17 -0
  143. data/lib/exlibris/primo_ws.rb +140 -0
  144. data/lib/generators/templates/umlaut_services.yml +237 -0
  145. data/lib/generators/umlaut/asset_hooks_generator.rb +44 -0
  146. data/lib/generators/umlaut/install_generator.rb +110 -0
  147. data/lib/hip3/bib.rb +291 -0
  148. data/lib/hip3/bib_searcher.rb +302 -0
  149. data/lib/hip3/custom_field_lookup.rb +44 -0
  150. data/lib/hip3/holding.rb +50 -0
  151. data/lib/hip3/item.rb +65 -0
  152. data/lib/hip3/receipt.rb +7 -0
  153. data/lib/hip3/serial_copy.rb +82 -0
  154. data/lib/holding.rb +32 -0
  155. data/lib/marc_helper.rb +254 -0
  156. data/lib/metadata_helper.rb +312 -0
  157. data/lib/opensearch_feed.rb +398 -0
  158. data/lib/opensearch_query.rb +98 -0
  159. data/lib/referent_filter.rb +16 -0
  160. data/lib/referent_filters/dissertation_catch.rb +45 -0
  161. data/lib/section_renderer.rb +503 -0
  162. data/lib/service.rb +336 -0
  163. data/lib/service_adaptors/ajax_export.rb +37 -0
  164. data/lib/service_adaptors/amazon.rb +412 -0
  165. data/lib/service_adaptors/blacklight.rb +327 -0
  166. data/lib/service_adaptors/book_finder.rb +40 -0
  167. data/lib/service_adaptors/bx.rb +51 -0
  168. data/lib/service_adaptors/cover_thing.rb +73 -0
  169. data/lib/service_adaptors/elsevier_cover.rb +57 -0
  170. data/lib/service_adaptors/email_export.rb +10 -0
  171. data/lib/service_adaptors/ezproxy.rb +171 -0
  172. data/lib/service_adaptors/google_book_search.rb +442 -0
  173. data/lib/service_adaptors/gpo.rb +124 -0
  174. data/lib/service_adaptors/hathi_trust.rb +308 -0
  175. data/lib/service_adaptors/hip3_service.rb +150 -0
  176. data/lib/service_adaptors/hip_holding_search.rb +237 -0
  177. data/lib/service_adaptors/internet_archive.rb +488 -0
  178. data/lib/service_adaptors/isbn_db.rb +86 -0
  179. data/lib/service_adaptors/isi.rb +258 -0
  180. data/lib/service_adaptors/jcr.rb +146 -0
  181. data/lib/service_adaptors/opac.rb +351 -0
  182. data/lib/service_adaptors/open_library.rb +316 -0
  183. data/lib/service_adaptors/open_library_cover.rb +73 -0
  184. data/lib/service_adaptors/primo_service.rb +392 -0
  185. data/lib/service_adaptors/primo_source.rb +78 -0
  186. data/lib/service_adaptors/pubmed.rb +133 -0
  187. data/lib/service_adaptors/request_to_fixture.rb +68 -0
  188. data/lib/service_adaptors/scopus.rb +295 -0
  189. data/lib/service_adaptors/sfx-new.rb +557 -0
  190. data/lib/service_adaptors/sfx.rb +566 -0
  191. data/lib/service_adaptors/sfx_backchannel_record.rb +69 -0
  192. data/lib/service_adaptors/txt_holding_export.rb +32 -0
  193. data/lib/service_adaptors/ulrichs_cover.rb +57 -0
  194. data/lib/service_adaptors/ulrichs_link.rb +47 -0
  195. data/lib/service_adaptors/worldcat.rb +116 -0
  196. data/lib/service_adaptors/worldcat_identities.rb +591 -0
  197. data/lib/tasks/umlaut.rake +134 -0
  198. data/lib/umlaut/default_configuration.rb +5 -0
  199. data/lib/umlaut/routes.rb +136 -0
  200. data/lib/umlaut/version.rb +3 -0
  201. data/lib/umlaut.rb +37 -0
  202. data/lib/umlaut_configurable.rb +343 -0
  203. data/lib/umlaut_http.rb +100 -0
  204. data/lib/xml_schema_helper.rb +109 -0
  205. data/test/dummy/Rakefile +7 -0
  206. data/test/dummy/app/assets/javascripts/application.js +13 -0
  207. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  208. data/test/dummy/app/controllers/application_controller.rb +3 -0
  209. data/test/dummy/app/controllers/umlaut_controller.rb +112 -0
  210. data/test/dummy/app/helpers/application_helper.rb +2 -0
  211. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  212. data/test/dummy/config/application.rb +45 -0
  213. data/test/dummy/config/boot.rb +10 -0
  214. data/test/dummy/config/database-jhu.yml +44 -0
  215. data/test/dummy/config/database.yml +25 -0
  216. data/test/dummy/config/environment.rb +5 -0
  217. data/test/dummy/config/environments/development.rb +34 -0
  218. data/test/dummy/config/environments/production.rb +60 -0
  219. data/test/dummy/config/environments/test.rb +39 -0
  220. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  221. data/test/dummy/config/initializers/inflections.rb +10 -0
  222. data/test/dummy/config/initializers/mime_types.rb +5 -0
  223. data/test/dummy/config/initializers/secret_token.rb +7 -0
  224. data/test/dummy/config/initializers/session_store.rb +8 -0
  225. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  226. data/test/dummy/config/locales/en.yml +5 -0
  227. data/test/dummy/config/routes.rb +61 -0
  228. data/test/dummy/config/umlaut_services.yml +237 -0
  229. data/test/dummy/config.ru +4 -0
  230. data/test/dummy/db/migrate/20111228211210_umlaut_init.rb +113 -0
  231. data/test/dummy/db/schema.rb +124 -0
  232. data/test/dummy/log/development.log +12981 -0
  233. data/test/dummy/log/production.log +0 -0
  234. data/test/dummy/public/404.html +26 -0
  235. data/test/dummy/public/422.html +26 -0
  236. data/test/dummy/public/500.html +26 -0
  237. data/test/dummy/public/favicon.ico +0 -0
  238. data/test/dummy/script/rails +6 -0
  239. data/test/dummy/tmp/cache/assets/C5F/340/sprockets%2F99692920160b7a279b86a80415b79db7 +0 -0
  240. data/test/dummy/tmp/cache/assets/C70/4D0/sprockets%2F034ad2036e623081bd352800786dfe80 +0 -0
  241. data/test/dummy/tmp/cache/assets/C73/920/sprockets%2Fd371318f22900492fd180f17c5e2a504 +9268 -0
  242. data/test/dummy/tmp/cache/assets/C80/980/sprockets%2Fc94807409c1523d43e18d25f35d93c41 +0 -0
  243. data/test/dummy/tmp/cache/assets/C8F/780/sprockets%2Fe47e28558116fb5f8038754e60d1961d +11769 -0
  244. data/test/dummy/tmp/cache/assets/CAA/EB0/sprockets%2F1d179210e8b76f1ea63c802688a015e4 +9271 -0
  245. data/test/dummy/tmp/cache/assets/CBB/9C0/sprockets%2F706f28923fb754cad04b9107c89986a1 +0 -0
  246. data/test/dummy/tmp/cache/assets/CBF/B60/sprockets%2F08ca89671549936265dcb673bf02e36f +0 -0
  247. data/test/dummy/tmp/cache/assets/CC9/9F0/sprockets%2F306166316e2cafd13c15e62b51a2339d +0 -0
  248. data/test/dummy/tmp/cache/assets/CF6/F20/sprockets%2F5b2ffa1103079dfd555197838f87a99f +0 -0
  249. data/test/dummy/tmp/cache/assets/CF7/2B0/sprockets%2F25a7c73655bd3598173b39d9f98bcd46 +862 -0
  250. data/test/dummy/tmp/cache/assets/CFE/080/sprockets%2F37fe9f4255baddbd549a659914929398 +0 -0
  251. data/test/dummy/tmp/cache/assets/D22/060/sprockets%2F9aec77b768e91a802d284271c58e2f7e +21357 -0
  252. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  253. data/test/dummy/tmp/cache/assets/D33/6D0/sprockets%2F500129c57f1146e556ec3aacd6cd38c1 +0 -0
  254. data/test/dummy/tmp/cache/assets/D33/FD0/sprockets%2F2ba0b4e6334a77b923e5f770381bb2bf +0 -0
  255. data/test/dummy/tmp/cache/assets/D42/C20/sprockets%2Fbcf14e437b1582bf93b77670acf8e090 +21353 -0
  256. data/test/dummy/tmp/cache/assets/D50/A30/sprockets%2F7d8b294ac433db5d056538f8cf7c66b9 +0 -0
  257. data/test/dummy/tmp/cache/assets/D54/ED0/sprockets%2F71c9fa01091d432b131da3bb73faf3d4 +872 -0
  258. data/test/dummy/tmp/cache/assets/D65/590/sprockets%2Fc1bb92fc3406a126b7dd302edc96d629 +0 -0
  259. data/test/dummy/tmp/cache/assets/D71/6B0/sprockets%2Fde558b71b494cf09b1bf055c8dff0353 +0 -0
  260. data/test/dummy/tmp/cache/assets/D72/610/sprockets%2Fa8c708eeb30ef93de34d755d4f45d023 +859 -0
  261. data/test/dummy/tmp/cache/assets/D76/AD0/sprockets%2Fe2158cde93188cf5ab6457bc6d6602ec +0 -0
  262. data/test/dummy/tmp/cache/assets/D7A/E40/sprockets%2F9622ffcc499a57627cd1bb18fe31b8e4 +11772 -0
  263. data/test/dummy/tmp/cache/assets/D84/210/sprockets%2Fabd0103ccec2b428ac62c94e4c40b384 +0 -0
  264. data/test/dummy/tmp/cache/assets/D9B/770/sprockets%2F8aacf02eb7dbb0949704b28f27b87e0b +0 -0
  265. data/test/dummy/tmp/cache/assets/DA6/A80/sprockets%2F92e26d8e58d5bcc8b8f6c25d1b05b9c1 +0 -0
  266. data/test/dummy/tmp/cache/assets/DE8/790/sprockets%2Fd1333bde2b9aafcc712d11dd09ab35d8 +0 -0
  267. data/test/dummy/tmp/cache/assets/DF7/F30/sprockets%2F7bc16c4109b17fabe29f8ddbbf732d1c +374 -0
  268. data/test/dummy/tmp/cache/assets/E03/570/sprockets%2F493bdc0ac14cd4f57fdfe4253f992bde +0 -0
  269. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  270. data/test/dummy/tmp/cache/assets/E0B/4B0/sprockets%2F7988df51a61c81ce6ede4a2d4c8cce4f +377 -0
  271. data/test/dummy/tmp/cache/assets/E5F/960/sprockets%2Fdc007b6cad5c7ef08e33ec28cfff0ef6 +0 -0
  272. data/test/fixtures/dispatched_services.yml +5 -0
  273. data/test/fixtures/permalinks.yml +5 -0
  274. data/test/fixtures/referent_values.yml +1734 -0
  275. data/test/fixtures/referents.yml +156 -0
  276. data/test/fixtures/requests.yml +284 -0
  277. data/test/fixtures/service_responses.yml +5 -0
  278. data/test/fixtures/sfx_urls.yml +4 -0
  279. data/test/performance/browsing_test.rb +9 -0
  280. data/test/test_helper.rb +10 -0
  281. data/test/umlaut_test.rb +7 -0
  282. data/test/unit/aleph_patron_test.rb +39 -0
  283. data/test/unit/aleph_record_benchmarks.rb +28 -0
  284. data/test/unit/aleph_record_test.rb +30 -0
  285. data/test/unit/aws_product_sign_test.rb +93 -0
  286. data/test/unit/collection_test.rb +76 -0
  287. data/test/unit/google_book_search_test.rb +101 -0
  288. data/test/unit/primo_searcher_test.rb +403 -0
  289. data/test/unit/primo_service_test.rb +939 -0
  290. data/test/unit/primo_ws_test.rb +131 -0
  291. data/test/unit/service_response_test.rb +9 -0
  292. data/test/unit/service_test.rb +33 -0
  293. metadata +580 -0
@@ -0,0 +1,327 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'base64'
4
+ require 'marc'
5
+
6
+ # Searches a Blacklight with the cql extension installed.
7
+ #
8
+ #
9
+ # Params include:
10
+ # [base_url]
11
+ # required. Complete URL to catalog.atom action. Eg "https://blacklight.mse.jhu.edu/catalog.atom"
12
+ # [bl_fields]
13
+ # required with at least some entries if you want this to do anything. Describe the names of given semantic fields in your BL instance.
14
+ # * issn
15
+ # * isbn
16
+ # * lccn
17
+ # * oclcnum
18
+ # * id (defaults to 'id')
19
+ # * title
20
+ # * author
21
+ # * serials_limit_clause => not an index name, full URL clause for a limit to apply to known serials searches, for instance "f[format][]=Serial"
22
+ # [identifier_search]
23
+ # Do catalog search on issn/isbn/oclcnum/lccn/bibId. Default true.
24
+ # [keyword_search]
25
+ # Do catalog search on title/author keywords where applicable. Generally only used when identifier_search finds no hits, if identifier_search is on. Default true.
26
+ # [keyword_per_page]
27
+ # How many records to fetch from blacklight when doing keyword searches.
28
+ # [exclude_holdings]
29
+ # Can be used to exclude certain 'dummy' holdings that have certain collection, location, or other values. Eg:
30
+ # exclude_holdings:
31
+ # collection_str:
32
+ # - World Wide Web
33
+ # - Internet
34
+ # [rft_id_bibnum_prefixes]
35
+ # Array of URI prefixes in an rft_id that indicate that the actual solr id comes next. For instance, if your blacklight will send "http://blacklight.com/catalog/some_id" in an rft_id, then include "http://blacklight.com/catalog/". Optional.
36
+ class Blacklight < Service
37
+ required_config_params :base_url, :display_name
38
+ attr_reader :base_url, :cql_search_field
39
+ attr_reader :bl_fields, :issn
40
+
41
+ include UmlautHttp
42
+ include MetadataHelper
43
+ include MarcHelper
44
+ include XmlSchemaHelper
45
+
46
+ def initialize(config)
47
+ # defaults
48
+ # If you are sending an OpenURL from a library service, you may
49
+ # have the HIP bibnum, and include it in the OpenURL as, eg.
50
+ # rft_id=http://catalog.library.jhu.edu/bib/343434 (except URL-encoded)
51
+ # Then you'd set rft_id_bibnum_prefix to http://catalog.library.jhu.edu/bib/
52
+ @rft_id_bibnum_prefixes = []
53
+ @cql_search_field = "cql"
54
+ @keyword_per_page = 10
55
+ @identifier_search = true
56
+ @keyword_search = true
57
+ @link_to_search = true
58
+ super(config)
59
+ @bl_fields = { "id" => "id "}.merge(@bl_fields)
60
+ end
61
+
62
+ # Standard method, used by background service updater. See Service docs.
63
+ def service_types_generated
64
+ types = [ ServiceTypeValue[:fulltext], ServiceTypeValue[:holding], ServiceTypeValue[:table_of_contents], ServiceTypeValue[:relevant_link] ]
65
+
66
+ return types
67
+ end
68
+
69
+
70
+ def handle(request)
71
+ ids_processed = []
72
+ holdings_added = 0
73
+
74
+ if (@identifier_search && url = blacklight_precise_search_url(request) )
75
+ doc = Nokogiri::XML( http_fetch(url).body )
76
+
77
+ ids_processed.concat( bib_ids_from_atom_entries( doc.xpath("atom:feed/atom:entry", xml_ns) ) )
78
+
79
+ # namespaces make xpath harder than it should be, but css
80
+ # selector still easy, thanks nokogiri! Grab the marc from our
81
+ # results.
82
+ marc_matches = doc.xpath("atom:feed/atom:entry/atom:content[@type='application/marc']", xml_ns).collect do |encoded_marc21|
83
+ MARC::Reader.decode( Base64.decode64(encoded_marc21.text) )
84
+ end
85
+
86
+ add_856_links(request, marc_matches )
87
+
88
+ # Got to make a second fetch for dlf_expanded info, cause BL doens't
89
+ # (yet) let us ask for more than one at once
90
+ holdings_url = blacklight_precise_search_url( request, "dlf_expanded" )
91
+ holdings_added += add_holdings( holdings_url ) if holdings_url
92
+ end
93
+ #keyword search.
94
+ if (@keyword_search &&
95
+ url = blacklight_keyword_search_url(request))
96
+
97
+ doc = Nokogiri::XML( http_fetch(url).body )
98
+ # filter out matches whose titles don't really match at all, or
99
+ # which have already been seen in identifier search.
100
+ entries = filter_keyword_entries( doc.xpath("atom:feed/atom:entry", xml_ns) , :exclude_ids => ids_processed, :remove_subtitle => (! title_is_serial?(request.referent)) )
101
+
102
+ marc_by_atom_id = {}
103
+
104
+ # Grab the marc from our entries. Important not to do a // xpath
105
+ # search, or we'll wind up matching parent elements not actually
106
+ # included in our 'entries' list.
107
+ marc_matches = entries.xpath("atom:content[@type='application/marc']", xml_ns).collect do |encoded_marc21|
108
+ marc = MARC::Reader.decode( Base64.decode64(encoded_marc21.text) )
109
+
110
+ marc_by_atom_id[ encoded_marc21.at_xpath("ancestor::atom:entry/atom:id/text()", xml_ns).to_s ] = marc
111
+
112
+ marc
113
+ end
114
+
115
+ # We've filtered out those we consider just plain bad
116
+ # matches, everything else we're going to call
117
+ # an approximate match. Sort so that those with
118
+ # a date close to our request date are first.
119
+ if ( year = get_year(request.referent))
120
+ marc_matches = marc_matches.partition {|marc| get_years(marc).include?( year )}.flatten
121
+ end
122
+ # And add in the 856's
123
+ add_856_links(request, marc_matches, :match_reliability => ServiceResponse::MatchUnsure)
124
+
125
+ # Fetch and add in the holdings
126
+ url = blacklight_url_for_ids(bib_ids_from_atom_entries(entries))
127
+
128
+ holdings_added += add_holdings( url, :match_reliability => ServiceResponse::MatchUnsure, :marc_data => marc_by_atom_id ) if url
129
+
130
+ if (@link_to_search && holdings_added ==0)
131
+ hit_count = doc.at_xpath("atom:feed/opensearch:totalResults/text()", xml_ns).to_s.to_i
132
+ html_result_url = doc.at_xpath("atom:feed/atom:link[@rel='alternate'][@type='text/html']/attribute::href", xml_ns).to_s
133
+
134
+ if hit_count > 0
135
+ request.add_service_response(
136
+ :service => self,
137
+ :source_name => @display_name,
138
+ :count => hit_count,
139
+ :display_text => "#{hit_count} possible #{case; when hit_count > 1 ; 'matches' ; else; 'match' ; end} in #{@display_name}",
140
+ :url => html_result_url,
141
+ :service_type_value => :holding_search )
142
+ end
143
+ end
144
+ end
145
+
146
+
147
+
148
+
149
+ return request.dispatched(self, true)
150
+
151
+
152
+ end
153
+
154
+ # Send a CQL request for any identifiers present.
155
+ # Ask for for an atom response with embedded marc21 back.
156
+ def blacklight_precise_search_url(request, format = "marc")
157
+ # Add search clauses for our identifiers, if we have them and have a configured search field for them.
158
+ clauses = []
159
+ added = []
160
+ ["lccn", "isbn", "oclcnum"].each do |key|
161
+ if bl_fields[key] && request.referent.send(key)
162
+ clauses.push( "#{bl_fields[key]} = \"#{request.referent.send(key)}\"")
163
+ added << key
164
+ end
165
+ end
166
+ # Only add ISSN if we don't have an ISBN, reduces false matches
167
+ if ( !added.include?("isbn") &&
168
+ bl_fields["issn"] &&
169
+ request.referent.issn)
170
+ clauses.push("#{bl_fields["issn"]} = \"#{request.referent.issn}\"")
171
+ end
172
+
173
+
174
+ # Add Solr document identifier if we can get one from the URL
175
+
176
+ if (id = get_solr_id(request.referent))
177
+ clauses.push("#{bl_fields['id']} = \"#{id}\"")
178
+ end
179
+
180
+ # if we have nothing, we can do no search.
181
+ return nil if clauses.length == 0
182
+
183
+ cql = clauses.join(" OR ")
184
+
185
+ return base_url + "?search_field=#{@cql_search_field}&content_format=#{format}&q=#{CGI.escape(cql)}"
186
+ end
187
+
188
+ # Construct a CQL search against blacklight for author and title,
189
+ # possibly with serial limit. Ask for Atom with embedded MARC back.
190
+ def blacklight_keyword_search_url(request, options = {})
191
+ options[:format] ||= "atom"
192
+ options[:content_format] ||= "marc"
193
+
194
+ clauses = []
195
+
196
+ # We need both title and author to search keyword style, or
197
+ # we get too many false positives. Except serials we'll do
198
+ # title only. sigh, logic tree.
199
+ title = get_search_title(request.referent)
200
+ author = get_top_level_creator(request.referent)
201
+ return nil unless title && (author || (@bl_fields["serials_limit_clause"] && title_is_serial?(request.referent)))
202
+ # phrase search for title, just raw dismax for author
203
+ # Embed quotes inside the quoted value, need to backslash-quote for CQL,
204
+ # and backslash the backslashes for ruby literal.
205
+ clauses.push("#{@bl_fields["title"]} = \"\\\"#{title}\\\"\"")
206
+ clauses.push("#{@bl_fields["author"]} = \"#{author}\"") if author
207
+
208
+
209
+
210
+ url = base_url + "?search_field=#{@cql_search_field}&content_format=#{options[:content_format]}&q=#{CGI.escape(clauses.join(" AND "))}"
211
+
212
+ if (@bl_fields["serials_limit_clause"] &&
213
+ title_is_serial?(request.referent))
214
+ url += "&" + @bl_fields["serials_limit_clause"]
215
+ end
216
+
217
+ return url
218
+ end
219
+
220
+ # Takes a url that will return atom response of dlf_expanded content.
221
+ # Adds Umlaut "holding" ServiceResponses for dlf_expanded, as appropriate.
222
+ # Returns number of holdings added.
223
+ def add_holdings(holdings_url, options = {})
224
+ options[:match_reliability] ||= ServiceResponse::MatchExact
225
+ options[:marc_data] ||= {}
226
+
227
+ atom = Nokogiri::XML( http_fetch(holdings_url).body )
228
+ content_entries = atom.search("/atom:feed/atom:entry/atom:content", xml_ns)
229
+
230
+ # For each atom entry, find the dlf_expanded record. For each dlf_expanded
231
+ # record, take all of it's holdingsrec's if it has them, or all of it's
232
+ # items if it doesn't, and add them to list. We wind up with a list
233
+ # of mixed holdingsrec's and items.
234
+ holdings_xml = content_entries.collect do |dlf_expanded|
235
+ copies = dlf_expanded.xpath("dlf:record/dlf:holdings/dlf:holdingset/dlf:holdingsrec", xml_ns)
236
+ copies.length > 0 ? copies : dlf_expanded.xpath("dlf:record/dlf:items/dlf:item", xml_ns)
237
+ end.flatten
238
+
239
+ service_data = holdings_xml.collect do | xml_metadata |
240
+ atom_entry = xml_metadata.at_xpath("ancestor::atom:entry", xml_ns)
241
+ atom_id = atom_entry.at_xpath("atom:id/text()", xml_ns).to_s
242
+
243
+ edition_str = edition_statement(options[:marc_data][atom_id])
244
+ url = atom_entry.at_xpath("atom:link[@rel='alternate'][@type='text/html']/attribute::href", xml_ns).to_s
245
+
246
+ xml_to_holdings( xml_metadata ).merge(
247
+ :service => self,
248
+ :match_reliability => options[:match_reliability],
249
+ :edition_str => edition_str,
250
+ :url => url
251
+ )
252
+ end
253
+
254
+ # strip out holdings that aren't really holdings
255
+ service_data.delete_if do |data|
256
+ @exclude_holdings.collect do |key, values|
257
+ values.include?(data[key.to_sym])
258
+ end.include?(true)
259
+ end
260
+
261
+ # Sort by "collection"
262
+ service_data.sort do |a, b|
263
+ a[:collection_str] <=> b[:collection_str]
264
+ end
265
+
266
+ service_data.each do |data|
267
+ request.add_service_response(data.merge(:service => self, :service_type_value =>"holding"))
268
+ end
269
+
270
+ return service_data.length
271
+ end
272
+
273
+ def filter_keyword_entries(atom_entries, options = {})
274
+ options[:exclude_ids] ||= []
275
+ options[:remove_subtitle] ||= true
276
+ request_title_forms = [
277
+ raw_search_title(request.referent).downcase,
278
+ normalize_title( raw_search_title(request.referent) )
279
+ ]
280
+ request_title_forms << normalize_title( raw_search_title(request.referent), :remove_subtitle => true) if options[:remove_subtitle]
281
+ request_title_forms.compact
282
+
283
+ # Only keep entries with title match, and that aren't in the
284
+ # exclude_ids list.
285
+ good_entries = atom_entries.find_all do |atom_entry|
286
+ title = atom_entry.xpath("atom:title/text()", xml_ns).to_s
287
+
288
+ entry_title_forms = [
289
+ title.downcase,
290
+ normalize_title(title)
291
+ ]
292
+ entry_title_forms << normalize_title(title, :remove_subtitle=>true) if options[:remove_subtitle]
293
+ entry_title_forms.compact
294
+
295
+ ((entry_title_forms & request_title_forms).length > 0 &&
296
+ (bib_ids_from_atom_entries(atom_entry) & options[:exclude_ids]).length == 0)
297
+ end
298
+ return Nokogiri::XML::NodeSet.new( atom_entries.document, good_entries)
299
+ end
300
+
301
+ def bib_ids_from_atom_entries(entries)
302
+ entries.xpath("atom:id/text()", xml_ns).to_a.collect do |atom_id|
303
+ atom_id.to_s =~ /([^\/]+)$/
304
+ $1
305
+ end.compact
306
+ end
307
+
308
+ def blacklight_url_for_ids(ids, format="dlf_expanded")
309
+ return nil unless ids.length > 0
310
+
311
+ return base_url + "?search_field=#{@cql_search_field}&content_format=#{format}&q=" + CGI.escape("#{@bl_fields["id"]} any \"#{ids.join(" ")}\"")
312
+ end
313
+
314
+
315
+ def get_solr_id(rft)
316
+ rft.identifiers.each do |id|
317
+ @rft_id_bibnum_prefixes.each do |prefix|
318
+ if id[0, prefix.length] == prefix
319
+ return id[prefix.length, id.length]
320
+ end
321
+ end
322
+ end
323
+
324
+ return nil
325
+ end
326
+
327
+ end
@@ -0,0 +1,40 @@
1
+ # Link to BookFinder.com to compare online new and used prices for a book.
2
+ # Requires an ISBN.
3
+ # Does not a pre-check, just generates the link blind, but I think almost any
4
+ # ISBN will get results on BookFinder.
5
+ class BookFinder < Service
6
+ require 'isbn'
7
+
8
+ def initialize(config)
9
+ @display_text = "Compare online prices"
10
+ @display_name = "BookFinder.com"
11
+ # %s is where the ISBN goes
12
+ @url_template = 'http://www.bookfinder.com/search/?isbn=%s&st=xl&ac=qr'
13
+
14
+ super(config)
15
+ end
16
+
17
+ def service_types_generated
18
+ return [ServiceTypeValue['highlighted_link']]
19
+ end
20
+
21
+ def handle(umlaut_request)
22
+ isbn = umlaut_request.referent.isbn
23
+
24
+ # Unless we have a valid isbn, give up
25
+ return request.dispatched(self, true) unless isbn && ISBN.valid?(isbn)
26
+
27
+ # Okay, make a link
28
+ url = @url_template.sub('%s', isbn)
29
+
30
+ umlaut_request.add_service_response(
31
+ :service=>self,
32
+ :url=> url,
33
+ :display_text=> @display_text,
34
+ :service_type_value => :highlighted_link
35
+ )
36
+
37
+ return request.dispatched(self, true)
38
+ end
39
+
40
+ end
@@ -0,0 +1,51 @@
1
+ # Uses bX recommender service to generate links to "similar" articles.
2
+ #
3
+ # REQUIREMENTS: You must be an bX customer. Required field "token" is specific to your institution.
4
+ # Optional fields:
5
+ # max_records - max number of records returned; default 10
6
+ # threshold - minimum score a recommendation must have in order to be included in the results; scores range from 0 to 100; default 50
7
+ # openurl_base - base for other link resolver; default "/resolve"
8
+ # source - values "local"|"global"; default "global"
9
+ class Bx < Service
10
+ require 'open-uri'
11
+ require 'nokogiri'
12
+
13
+ required_config_params :token
14
+
15
+ def initialize(config)
16
+ @display_name = "Bx"
17
+ @base_url = "http://recommender.service.exlibrisgroup.com/service/recommender/openurl"
18
+ @max_records = "5"
19
+ @threshold = "50"
20
+ @source = "global"
21
+ @openurl_base = "/resolve"
22
+ super(config)
23
+ end
24
+
25
+ def service_types_generated
26
+ return [ServiceTypeValue[:similar]]
27
+ end
28
+
29
+ def handle(request)
30
+ format = "rss"
31
+ bx_url = "#{@base_url}?res_dat=format%3D#{format}%26source%3D#{@source}%26token%3D#{@token}%26maxRecords%3D#{@max_records}%26threshold%3D#{@threshold}%26baseUrl%3D#{@openurl_base}&#{request.to_context_object.kev}"
32
+ response = open(bx_url)
33
+ Rails.logger.debug("bX URL #{bx_url.inspect}")
34
+ response_xml_str = response.read
35
+ Rails.logger.debug("bX Response #{response_xml_str.inspect}")
36
+ response_xml = Nokogiri::XML(response_xml_str)
37
+ response_xml.search("//item") do |item|
38
+ title = item.at("title").inner_text
39
+ author = item.at("author").inner_text
40
+ display_text = (author.nil?)? "#{title}" : "#{author}; #{title}"
41
+ url = item.at("link").inner_text
42
+ request.add_service_response(
43
+ :service=>self,
44
+ :display_text => display_text,
45
+ :url => url,
46
+ :service_type_value => :similar)
47
+ end
48
+ return request.dispatched(self, true)
49
+ end
50
+
51
+ end
@@ -0,0 +1,73 @@
1
+ # Find book covers from LibraryThing's CoverThing service.
2
+ # Only fetches "medium" size image. Fetches only by ISBN.
3
+
4
+ class CoverThing < Service
5
+ require 'net/http'
6
+ include MetadataHelper
7
+ required_config_params :developer_key
8
+
9
+ def service_types_generated
10
+ return [ ServiceTypeValue[:cover_image] ]
11
+ end
12
+
13
+ def initialize(config)
14
+ @display_name = "LibraryThing"
15
+ # http://covers.librarything.com/devkey/KEY/medium/isbn/0545010225
16
+ @base_url = 'http://covers.librarything.com/devkey/';
17
+ @lt404url = 'http://www.librarything.com/coverthing404.php'
18
+
19
+ @credits = {
20
+ "LibraryThing" => "http://www.librarything.com/"
21
+ }
22
+
23
+ super(config)
24
+ end
25
+
26
+ def handle(request)
27
+ image_url = image_url(request.referent)
28
+ return request.dispatched(self, true) unless image_url
29
+
30
+ uri = URI.parse(image_url)
31
+ response = nil
32
+ # All we need is a HEAD request to check content-length.
33
+ Net::HTTP.start(uri.host, uri.port) {|http|
34
+ response = http.head(uri.path)
35
+ }
36
+
37
+ # Only way to know if we got an image or a transparent placeholder
38
+ # is to check the content-length. Currently the transparent placeholder
39
+ # is 43 bytes. -- not true anymore, now we can check for a redirect,
40
+ # I guess.
41
+
42
+ # Not sure why response is ever nil, but sometimes it is, let's log
43
+ # some info.
44
+ if ( response.kind_of?(Net::HTTPRedirection) && response["location"] == @lt404url)
45
+ # no cover found.
46
+ return request.dispatched(self, true)
47
+ elsif ( response.nil? || response.content_length.nil? )
48
+ Rails.logger.debug("CoverThing: Null response for #{uri}, status #{response.class}")
49
+ end
50
+ unless (response.nil? || response.content_length.nil? || response.content_length < 50)
51
+ request.add_service_response(
52
+ :service=>self,
53
+ :display_text => 'Cover Image',
54
+ :key=> 'medium',
55
+ :url => image_url,
56
+ :size => 'medium',
57
+ :service_type_value => :cover_image
58
+ )
59
+ end
60
+
61
+ return request.dispatched(self, true)
62
+ end
63
+
64
+ def image_url(referent)
65
+ isbn = get_identifier(:urn, "isbn", referent)
66
+ isbn.gsub!(/[^\d]/, '') if isbn # just numeric isbn
67
+ return nil if isbn.blank? # need an isbn to make the request
68
+
69
+ return @base_url + @developer_key + '/medium/isbn/' + isbn
70
+ end
71
+
72
+
73
+ end