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,566 @@
1
+ # NOTE: In your SFX Admin, under Menu Configuration / API, you should enable ALL
2
+ # 'extra' API information for full umlaut functionality.
3
+ # With the exception of "Include openURL parameter", can't figure out how
4
+ # that's useful.
5
+ #
6
+ # config parameters in services.yml
7
+ # display name: User displayable name for this service
8
+ # base_url: SFX base url.
9
+ # click_passthrough: DEPRECATED. Caused problems. Use the SFXBackchannelRecord
10
+ # link filter service instead.
11
+ # When set to true, Umlaut will send all SFX clicks
12
+ # through SFX, for SFX to capture statistics. This is currently done
13
+ # using a backdoor into the SFX sfxresolve.cgi script. Defaults to false.
14
+ # Note that
15
+ # after SFX requests have been removed in the nightly job, the
16
+ # click passthrough will cause an error! Set sfx_requests_expire_crontab
17
+ # with the crontab pattern you use for requests to expire, and we won't
18
+ # try to click passthrough with expired requests.
19
+ # sfx_requests_expire_crontab: Crontab pattern that the SFX admin is using
20
+ # to expire SFX requests. Used to refrain from click passthrough with
21
+ # expired requests, since that is destined to fail.
22
+ # services_of_interest: Optional. over-ride the built in list of what types of
23
+ # SFX services we want to grab, and what the corresponding umlaut types are.
24
+ # hash, with SFX service type name as key, and Umlaut ServiceTypeValue
25
+ # name as value.
26
+ # extra_targets_of_interest: sfx target_names of targets you want to make
27
+ # sure to include in umlaut. A hash with target_name as key, and umlaut
28
+ # ResponseTypeValue name as value.
29
+ # really_distant_relationships: An array of relationship type codes from SFX
30
+ # "related objects". See Table 18 in SFX 3.0 User's Manual. Related
31
+ # objects that have only a "really distant relationship" will NOT
32
+ # be shown as fulltext, but will instead be banished to the see also
33
+ # "highlighted_link" section. You must have display of related objects
34
+ # turned ON in SFX display admin, to get related objects at all in
35
+ # Umlaut. NOTE: This parameter has a default value to a certain set of
36
+ # relationships, set to empty array [] to eliminate defaults.
37
+ # sfx_timeout: in seconds, for both open/read timeout value for SFX connection.
38
+ # Defaults to 8.
39
+ class Sfx < Service
40
+ require 'uri'
41
+ require 'htmlentities'
42
+ require 'cgi'
43
+ require 'nokogiri'
44
+
45
+ #require 'open_url'
46
+
47
+ required_config_params :base_url
48
+
49
+ def initialize(config)
50
+
51
+ # Key is sfx service_type, value is umlaut servicetype string.
52
+ # These are the SFX service types we will translate to umlaut
53
+ @services_of_interest = {'getFullTxt' => 'fulltext',
54
+ 'getSelectedFullTxt' => 'fulltext',
55
+ 'getDocumentDelivery' => 'document_delivery',
56
+ 'getDOI' => 'highlighted_link',
57
+ 'getAbstract' => 'abstract',
58
+ 'getTOC' => 'table_of_contents'}
59
+
60
+ # Special targets. Key is SFX target_name.
61
+ # Value is umlaut service type.
62
+ # These targets will be included even if their sfx service_type doesn't
63
+ # match our services_of_interest, and the umlaut service ID string given
64
+ # here will take precedence and be used even if these targets DO match
65
+ # services_of_interest. Generally loaded from yml config in super.
66
+ @extra_targets_of_interest = {}
67
+
68
+ @sfx_timeout = 8
69
+
70
+ @really_distant_relationships = ["CONTINUES_IN_PART", "CONTINUED_IN_PART_BY", "ABSORBED_IN_PART", "ABSORBED_BY"]
71
+
72
+ # Include a CrossRef credit, becuase SFX uses CrossRef api internally,
73
+ # and CrossRef ToS may require us to give credit.
74
+ @credits = {
75
+ "SFX" => "http://www.exlibrisgroup.com/sfx.htm",
76
+ "CrossRef" => "http://www.crossref.org/"
77
+ }
78
+
79
+ super(config)
80
+ end
81
+
82
+ # Standard method, used by auto background updater. See Service docs.
83
+ def service_types_generated
84
+ service_strings = []
85
+ service_strings.concat( @services_of_interest.values() )
86
+ service_strings.concat( @extra_targets_of_interest.values() )
87
+ service_strings.uniq!
88
+
89
+ return service_strings.collect { |s| ServiceTypeValue[s] }
90
+ end
91
+
92
+ def base_url
93
+ return @base_url
94
+ end
95
+
96
+ def handle(request)
97
+ client = self.initialize_client(request)
98
+ begin
99
+ response = self.do_request(client)
100
+ self.parse_response(response, request)
101
+ return request.dispatched(self, true)
102
+ rescue Errno::ETIMEDOUT, Timeout::Error => e
103
+ # Request to SFX timed out. Record this as unsuccessful in the dispatch table. Temporary.
104
+ return request.dispatched(self, DispatchedService::FailedTemporary, e)
105
+ end
106
+ end
107
+
108
+ def initialize_client(request)
109
+ transport = OpenURL::Transport.new(@base_url, nil, :open_timeout => @sfx_timeout, :read_timeout => @sfx_timeout)
110
+
111
+ context_object = request.to_context_object
112
+ transport.add_context_object(context_object)
113
+ transport.extra_args["sfx.response_type"]="multi_obj_xml"
114
+
115
+ @get_coverage = false
116
+
117
+ metadata = request.referent.metadata
118
+ if ( metadata['date'].blank? &&
119
+ metadata['year'].blank? &&
120
+ (! request.referent.identifiers.find {|i| i =~ /^info\:(doi|pmid)/})
121
+ )
122
+ # No article-level metadata, do some special stuff.
123
+ transport.extra_args["sfx.ignore_date_threshold"]="1"
124
+ transport.extra_args["sfx.show_availability"]="1"
125
+ @get_coverage = true
126
+ end
127
+ # Workaround to SFX bug, not sure if this is really still neccesary
128
+ # I think it's not, but leave it in anyway just in case.
129
+ if (context_object.referent.identifiers.find {|i| i =~ /^info:doi\// })
130
+ transport.extra_args['sfx.doi_url']='http://dx.doi.org'
131
+ end
132
+
133
+ return transport
134
+ end
135
+
136
+ def do_request(client)
137
+ client.transport_inline
138
+ return client.response
139
+ end
140
+
141
+ def parse_response(resolver_response, request)
142
+ doc = Nokogiri::XML(resolver_response)
143
+
144
+ # Catch an SFX error message (in HTML) that's not an XML
145
+ # document at all.
146
+ unless doc.at('/ctx_obj_set')
147
+ Rails.logger.error("sfx.rb: SFX did not return expected response. SFX response: #{resolver_response}")
148
+ raise "SFX did not return expected response."
149
+ end
150
+
151
+ # There can be several context objects in the response.
152
+ # We need to keep track of which data comes from which, for
153
+ # SFX click-through generating et alia
154
+ sfx_objs = doc.search('/ctx_obj_set/ctx_obj')
155
+
156
+ # As we go through the possibly multiple SFX context objects,
157
+ # we need to keep track of which one, if any, we want to use
158
+ # to enhance the Umlaut referent metadata.
159
+ #
160
+ # We only enhance for journal type metadata. For book type
161
+ # metadata SFX will return something, but it may not be the manifestation
162
+ # we want. With journal titles, less of an issue.
163
+ #
164
+ # In case of multiple SFX hits, enhance metadata only from the
165
+ # one that actually had fulltext. If more than one had fulltext, forget it,
166
+ # too error prone. If none had full text, just pick the first.
167
+ #
168
+ # We'll use these variables to keep track of our 'best fit' as
169
+ # we loop through em.
170
+ best_fulltext_ctx = nil
171
+ best_nofulltext_ctx = nil
172
+
173
+ # We're going to keep our @really_distant_relationship stuff here.
174
+ related_titles = {}
175
+
176
+ 0.upto(sfx_objs.length - 1 ) do |sfx_obj_index|
177
+
178
+ sfx_obj = sfx_objs[sfx_obj_index]
179
+
180
+ # Get out the "perl_data" section, with our actual OpenURL style
181
+ # context object information. This was XML escaped as a String (actually
182
+ # double-escaped, weirdly), so
183
+ # we need to extract the string, unescape it, and then feed it to Nokogiri
184
+ # again.
185
+ ctx_obj_atts =
186
+ CGI.unescapeHTML( sfx_obj.at('./ctx_obj_attributes').inner_html)
187
+
188
+ perl_data = Nokogiri::XML( ctx_obj_atts )
189
+ # parse it into an OpenURL, we might need it like that.
190
+ sfx_co = Sfx.parse_perl_data(perl_data)
191
+ sfx_metadata = sfx_co.to_hash
192
+
193
+
194
+ # get SFX objectID
195
+ object_id_node =
196
+ perl_data.at("./perldata/hash/item[@key='rft.object_id']")
197
+ object_id = object_id_node ? object_id_node.inner_html : nil
198
+
199
+ # Get SFX requestID
200
+ request_id_node =
201
+ perl_data.at("./perldata/hash/item[@key='sfx.request_id']")
202
+ request_id = request_id_node ? request_id_node.inner_html : nil
203
+
204
+ # Get targets service ids
205
+ sfx_target_service_ids =
206
+ sfx_obj.search('ctx_obj_targets/target/target_service_id').collect {|e| e.inner_html}
207
+
208
+
209
+
210
+ metadata = request.referent.metadata
211
+
212
+ # For each target delivered by SFX
213
+ sfx_obj.search("./ctx_obj_targets/target").each_with_index do|target, target_index|
214
+ response_data = {}
215
+
216
+ # First check @extra_targets_of_interest
217
+ sfx_target_name = target.at('./target_name').inner_html
218
+ umlaut_service = @extra_targets_of_interest[sfx_target_name]
219
+
220
+ # If not found, look for it in services_of_interest
221
+ unless ( umlaut_service )
222
+ sfx_service_type = target.at("./service_type").inner_html
223
+ umlaut_service = @services_of_interest[sfx_service_type]
224
+ end
225
+
226
+ # If we have multiple context objs, skip the ill and ask-a-librarian
227
+ # links for all but the first, to avoid dups. This is a bit messy,
228
+ # but this whole multiple hits thing is messy.
229
+ if ( sfx_obj_index > 0 &&
230
+ ( umlaut_service == 'document_delivery' ||
231
+ umlaut_service == 'export_citation' ||
232
+ umlaut_service == 'help'))
233
+ next
234
+ end
235
+
236
+
237
+ # Okay, keep track of best fit ctx for metadata enhancement
238
+ if request.referent.format == "journal"
239
+ if ( umlaut_service == 'fulltext')
240
+ best_fulltext_ctx = perl_data
241
+ best_nofulltext_ctx = nil
242
+ elsif best_nofulltext_ctx == nil
243
+ best_nofulltext_ctx = perl_data
244
+ end
245
+ end
246
+
247
+ if ( umlaut_service ) # Okay, it's in services or targets of interest
248
+ if (target/"./displayer")
249
+ source = "SFX/"+(target/"./displayer").inner_html
250
+ else
251
+ source = "SFX"+URI.parse(self.url).path
252
+ end
253
+
254
+ target_service_id = (target/"./target_service_id").inner_html
255
+
256
+ coverage = nil
257
+ if ( @get_coverage )
258
+ # Make sure you turn on "Include availability info in text format"
259
+ # in the SFX Admin API configuration.
260
+ thresholds_str = ""
261
+ target.search('coverage/coverage_text/threshold_text/coverage_statement').each do | threshold |
262
+ thresholds_str += threshold.inner_html.to_s + ".\n";
263
+ end
264
+
265
+ embargoes_str = "";
266
+ target.search('coverage/coverage_text/embargo_text/embargo_statement').each do |embargo |
267
+ embargoes_str += embargo.inner_html.to_s + ".\n";
268
+ end
269
+
270
+ unless ( thresholds_str.blank? && embargoes_str.blank? )
271
+ coverage = thresholds_str + embargoes_str
272
+ end
273
+ end
274
+
275
+
276
+ related_note = ""
277
+ # If this is from a related object, add that on as a note too...
278
+ # And maybe skip this entirely!
279
+ if (related_node = target.at('./related_service_info'))
280
+ relationship = related_node.at('./relation_type').inner_text
281
+ issn = related_node.at('./related_object_issn').inner_text
282
+ sfx_object_id = related_node.at('./related_object_id').inner_text
283
+ title = related_node.at('./related_object_title').inner_text
284
+
285
+ if @really_distant_relationships.include?(
286
+ related_node.at('./relation_type').inner_text)
287
+ # Show title-level link in see-also instead of full text.
288
+ related_titles[issn] = {
289
+ :sfx_object_id => sfx_object_id,
290
+ :title => title,
291
+ :relationship => relationship,
292
+ :issn => issn
293
+ }
294
+
295
+ next
296
+ end
297
+
298
+ related_note = "This version provided from related title: <i>" + CGI.unescapeHTML( title ) + "</i>.\n"
299
+ end
300
+
301
+ if ( sfx_service_type == 'getDocumentDelivery' )
302
+ value_string = request_id
303
+ else
304
+ value_string = (target/"./target_service_id").inner_html
305
+ end
306
+
307
+ response_data[:url] = CGI.unescapeHTML((target/"./target_url").inner_html)
308
+ response_data[:notes] = related_note.to_s + CGI.unescapeHTML((target/"./note").inner_html)
309
+ response_data[:authentication] = CGI.unescapeHTML((target/"./authentication").inner_html)
310
+ response_data[:source] = source
311
+ response_data[:coverage] = coverage if coverage
312
+
313
+ # Sfx metadata we want
314
+ response_data[:sfx_base_url] = @base_url
315
+ response_data[:sfx_obj_index] = sfx_obj_index + 1 # sfx is 1 indexed
316
+ response_data[:sfx_target_index] = target_index + 1
317
+ response_data[:sfx_request_id] = (perl_data/"//hash/item[@key='sfx.request_id']").first.inner_html
318
+ response_data[:sfx_target_service_id] = target_service_id
319
+ response_data[:sfx_target_name] = sfx_target_name
320
+ # At url-generation time, the request isn't available to us anymore,
321
+ # so we better store this citation info here now, since we need it
322
+ # for sfx click passthrough
323
+
324
+ # Oops, need to take this from SFX delivered metadata.
325
+
326
+ response_data[:citation_year] = sfx_metadata['rft.date'].to_s[0,4] if sfx_metadata['rft.date']
327
+ response_data[:citation_volume] = sfx_metadata['rft.volume'];
328
+ response_data[:citation_issue] = sfx_metadata['rft.issue']
329
+ response_data[:citation_spage] = sfx_metadata['rft.spage']
330
+
331
+ # Some debug info
332
+ response_data[:debug_info] =" Target: #{sfx_target_name} ; SFX object ID: #{object_id}"
333
+
334
+ response_data[:display_text] = (target/"./target_public_name").inner_html
335
+
336
+ request.add_service_response(
337
+ response_data.merge(
338
+ :service => self,
339
+ :service_type_value => umlaut_service
340
+ ))
341
+
342
+
343
+
344
+ end
345
+ end
346
+ end
347
+
348
+ # Add in links to our related titles
349
+ related_titles.each_pair do |issn, hash|
350
+ request.add_service_response(
351
+ :service => self,
352
+ :display_text => "#{sfx_relationship_display(hash[:relationship])}: #{hash[:title]}",
353
+ :notes => "#{ServiceTypeValue['fulltext'].display_name} available",
354
+ :related_object_hash => hash,
355
+ :service_type_value => "highlighted_link")
356
+ end
357
+
358
+ # Did we find a ctx best fit for enhancement?
359
+ if best_fulltext_ctx
360
+ enhance_referent(request, best_fulltext_ctx)
361
+ elsif best_nofulltext_ctx
362
+ enhance_referent(request, best_nofulltext_ctx)
363
+ end
364
+
365
+ end
366
+
367
+
368
+ def sfx_click_passthrough
369
+ # From config, or default to false.
370
+ return @click_passthrough || false;
371
+ end
372
+
373
+ # Using the value of sfx_request_expire_crontab, determine if the
374
+ # umlaut service response is so old that we can't use it for
375
+ # sfx click passthrough anymore.
376
+ def expired_sfx_request(response)
377
+ require 'CronTab'
378
+
379
+ crontab_str = @sfx_requests_expire_crontab
380
+
381
+ return false unless crontab_str # no param, no determination possible
382
+
383
+ crontab = CronTab.new( crontab_str )
384
+
385
+ time_of_response = response.created_at
386
+
387
+ return false unless time_of_response # no recorded time, not possible either
388
+
389
+ expire_time = crontab.nexttime( time_of_response )
390
+
391
+ # Give an extra five minutes of time, in case the expire
392
+ # process takes up to five minutes to finish.
393
+ return( Time.now > (expire_time + 5.minutes) )
394
+ end
395
+
396
+ # Try to provide a weird reverse-engineered url to take the user THROUGH
397
+ # sfx to their destination, so sfx will capture for statistics.
398
+ # This relies on certain information from the orignal sfx response
399
+ # being stored in the Response object at that point. Used by
400
+ # sfx_backchannel_record service.
401
+ def self.pass_through_url(response)
402
+ base_url = response[:sfx_base_url]
403
+
404
+ sfx_resolver_cgi_url = base_url + "/cgi/core/sfxresolver.cgi"
405
+
406
+
407
+ dataString = "?tmp_ctx_svc_id=#{response[:sfx_target_index]}"
408
+ dataString += "&tmp_ctx_obj_id=#{response[:sfx_obj_index]}"
409
+
410
+ # Don't understand what this is, but it sometimes needs to be 1?
411
+ # Hopefully it won't mess anything up when it's not neccesary.
412
+ # Really have no idea when it would need to be something other
413
+ # than 1.
414
+ # Nope, sad to say it does mess up cases where it is not neccesary.
415
+ # Grr.
416
+ #dataString += "&tmp_parent_ctx_obj_id=1"
417
+
418
+ dataString += "&service_id=#{response[:sfx_target_service_id]}"
419
+ dataString += "&request_id=#{response[:sfx_request_id]}"
420
+ dataString += "&rft.year="
421
+ dataString += URI.escape(response[:citation_year].to_s) if response[:citation_year]
422
+ dataString += "&rft.volume="
423
+ dataString += URI.escape(response[:citation_volume].to_s) if response[:citation_volume]
424
+ dataString += "&rft.issue="
425
+ dataString += URI.escape(response[:citation_issue].to_s) if response[:citation_issue]
426
+ dataString += "&rft.spage="
427
+ dataString += URI.escape(response[:citation_spage]).to_s if response[:citation_spage]
428
+
429
+ return sfx_resolver_cgi_url + dataString
430
+ end
431
+
432
+ # Class method to parse a perl_data block as XML in String
433
+ # into a ContextObject. Argument is Nokogiri doc containing
434
+ # the SFX <perldata> element and children.
435
+ def self.parse_perl_data(doc)
436
+
437
+ co = OpenURL::ContextObject.new
438
+ co.referent.set_format('journal') # default
439
+
440
+ html_ent_coder = HTMLEntities.new
441
+
442
+ doc.search('perldata/hash/item').each do |item|
443
+ key = item['key'].to_s
444
+
445
+ value = item.inner_html
446
+
447
+ # Some normalization. SFX uses rft.year, which is not actually
448
+ # legal. Stick it in rft.date instead.
449
+ key = "rft.date" if key == "rft.year"
450
+
451
+ prefix, stripped = key.split('.')
452
+
453
+ # The auinit1 value is COMPLETELY messed up for reasons I do not know.
454
+ # Double encoded in bizarre ways.
455
+ next if key == '@rft.auinit1' || key == '@rft.auinit'
456
+
457
+
458
+
459
+
460
+ # Darn multi-value SFX hackery, indicated with keys beginning
461
+ # with '@'. Just take the first one,
462
+ # our context object can't store more than one. Then regularize the
463
+ # key name.
464
+ if (prefix == '@rft')
465
+ array_items = item.search("array/item")
466
+ array_i = array_items[0] unless array_items.blank?
467
+
468
+ prefix = prefix.slice(1, prefix.length)
469
+ value = array_i ? array_i.inner_html : nil
470
+ end
471
+
472
+ # But this still has HTML entities in it sometimes. Now we've
473
+ # got to decode THAT.
474
+ value = html_ent_coder.decode(value)
475
+
476
+ # object_type? Fix that to be the right way.
477
+ if (prefix=='rft') && (key=='object_type')
478
+ co.referent.set_format( value.downcase )
479
+ next
480
+ end
481
+
482
+ if (prefix == 'rft' && value)
483
+ co.referent.set_metadata(stripped, value)
484
+ end
485
+
486
+ if (prefix=='@rft_id')
487
+ identifiers = item.search('array/item')
488
+ identifiers.each do |id|
489
+ co.referent.add_identifier(id.inner_html)
490
+ end
491
+ end
492
+ if (prefix=='@rfr_id')
493
+ identifiers = item.search('array/item')
494
+ identifiers.each do |id|
495
+ co.referrer.add_identifier(id.inner_html)
496
+ end
497
+ end
498
+ end
499
+ return co
500
+ end
501
+
502
+ # Custom url generation for the weird case
503
+ def response_url(service_response, submitted_params)
504
+ if (related_object = service_response.data_values[:related_object_hash])
505
+ {:controller => 'resolve', "rft.issn" => related_object[:issn], "rft.title" => related_object[:title], "rft.object_id" => related_object[:sfx_object_id] }
506
+ else
507
+ service_response['url']
508
+ end
509
+ end
510
+
511
+ protected
512
+ # Second argument is a Nokogiri element representing the <perldata>
513
+ # tag and children.
514
+ def enhance_referent(request, perl_data)
515
+ ActiveRecord::Base.connection_pool.with_connection do
516
+ metadata = request.referent.metadata
517
+
518
+ sfx_co = Sfx.parse_perl_data(perl_data)
519
+
520
+ sfx_metadata = sfx_co.referent.metadata
521
+ # Do NOT enhance for metadata type 'BOOK', unreliable matching from
522
+ # SFX!
523
+ return if sfx_metadata["object_type"] == "BOOK" || sfx_metadata["genre"] == "book"
524
+
525
+ # If we already had metadata for journal title and the SFX one
526
+ # differs, we want to over-write it. This is good for ambiguous
527
+ # incoming OpenURLs, among other things.
528
+
529
+ if request.referent.format == 'journal'
530
+ request.referent.enhance_referent("jtitle", sfx_metadata['jtitle'])
531
+ end
532
+ # And ISSN
533
+ if request.referent.format == 'journal' && ! sfx_metadata['issn'].blank?
534
+ request.referent.enhance_referent('issn', sfx_metadata['issn'])
535
+ end
536
+
537
+
538
+ # The rest, we write only if blank, we don't over-write
539
+ sfx_metadata.each do |key, value|
540
+ if (metadata[key].blank?)
541
+
542
+ # watch out for SFX's weird array values.
543
+ request.referent.enhance_referent(key, value)
544
+ end
545
+ end
546
+ end
547
+ end
548
+
549
+
550
+
551
+ # From Table 18 in SFX General User's Guide 3.0.
552
+ def sfx_relationship_display(sfx_code)
553
+ sfx_code = sfx_code.to_s
554
+ # Most can simply be #humanized, a couple of over-rides
555
+ @sfx_relationship_display ||= {
556
+ "TRANSLATION_ENTRY" => "Translation",
557
+ }
558
+
559
+ display = @sfx_relationship_display[sfx_code]
560
+ display = sfx_code.humanize if display.nil?
561
+
562
+ return display
563
+ end
564
+
565
+ end
566
+