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,351 @@
1
+ # FIXME This service is not working
2
+ # For starters it needs the method service_types_generated()
3
+
4
+ class Opac < Service
5
+ attr_reader :record_attributes, :display_name
6
+
7
+ # The Opac class has a few assumptions
8
+ # * You have some sort of bib client to get MARC records from your OPAC
9
+ # * This operates independently of holdings data
10
+ # * If your client can supply holdings information, you have the methods:
11
+ # - init_holdings_client
12
+ # - get_holdings
13
+ #
14
+ # The Opac class should be extended with your local connection client and
15
+ # not called directly.
16
+
17
+ def handle(request)
18
+ # It's unlikely that consortial catalogs allow borrowing
19
+ # of journals, but this may need to pushed to a client class
20
+
21
+ if request.referent.format == 'journal' and @consortial
22
+ return request.dispatched(self, true)
23
+ end
24
+ @record_attributes = {}
25
+ self.search_bib_data(request)
26
+ if self.respond_to?(:init_holdings_client)
27
+ opac_client = self.init_holdings_client
28
+ opac_client.get_holdings(@record_attributes.keys)
29
+ self.check_holdings(opac_client.results, request)
30
+ else
31
+ self.add_link_to_opac(client,request)
32
+ end
33
+ return request.dispatched(self, true)
34
+ end
35
+
36
+ # Searches the bibliographic database
37
+ def search_bib_data(request)
38
+ client = self.init_bib_client
39
+ client.search_by_referent(request.referent)
40
+ self.collect_record_attributes(client, request)
41
+ end
42
+
43
+ # The client is expected to respond to <b>results</b> and return
44
+ # an array of MARC records. Override this method (and parse_for_fulltext,
45
+ # collect_subjects, and enhance_referent) if your client does not return
46
+ # MARC.
47
+ def collect_record_attributes(client, request)
48
+ require 'marc'
49
+ client.results.each do | record |
50
+ MARC::XMLReader.new(StringIO.new(record.to_s)).each do | rec |
51
+ id = nil
52
+
53
+ # Assumes your local control number to be defined in 001
54
+ rec.find_all { | f| '001' === f.tag}.each do | bibnum |
55
+ id = bibnum.value
56
+ @record_attributes[id] = {}
57
+ end
58
+
59
+ # Conferences have their own crazy logic
60
+ if self.is_conference?(rec)
61
+ @record_attributes[id][:conference] = true
62
+ else
63
+ @record_attributes[id][:conference] = false
64
+ end
65
+ self.parse_for_fulltext_links(rec, request)
66
+ self.enhance_referent(rec, request, client.accuracy)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Searches the MARC record for 856 tags, checks to see
72
+ # if they seem to be digital representations and, if so,
73
+ # adds them as fulltext service types
74
+ def parse_for_fulltext_links(marc, request)
75
+ eight_fifty_sixes = []
76
+ marc.find_all { | f| '856' === f.tag}.each do | link |
77
+ eight_fifty_sixes << link
78
+ end
79
+ eight_fifty_sixes.each do | link |
80
+ next if link.indicator2.match(/[28]/)
81
+ next if link['u'].match(/(sfx\.galib\.uga\.edu)|(findit\.library\.gatech\.edu)/)
82
+ label = (link['z']||'Electronic Access')
83
+ request.add_service_response(
84
+ :service=>self,
85
+ :key=>label,
86
+ :value_string=>link['u'],
87
+ :service_type_value => 'fulltext'
88
+ )
89
+ end
90
+ end
91
+
92
+ # The client should be able to return an array of holdings objects
93
+ def check_holdings(holdings, request)
94
+ return if holdings.empty?
95
+ # These need to moved to services.yml
96
+ electronic_locations = ['INTERNET', 'NETLIBRARY', 'GALILEO']
97
+ holdings.each do | holding |
98
+ @record_attributes[holding.identifier.to_s][:holdings] = holding
99
+ holding.locations.each do | location |
100
+ next if electronic_locations.index(location.code)
101
+ location.items.each do | item |
102
+ if request.referent.format == 'journal' and request.referent.metadata["volume"] and @record_attributes[holding.identifier.to_s][:conference] == false
103
+ copy_match = false
104
+ if item.enumeration
105
+ if vol_match = item.enumeration.match(/VOL [A-z0-9\-]*/)
106
+ vol = vol_match[0]
107
+ vol.sub!(/^VOL\s*/, '')
108
+ (svol, evol) = vol.split('-')
109
+ if request.referent.metadata["volume"] == svol
110
+ copy_match = true
111
+ elsif evol
112
+ if request.referent.metadata["volume"] == evol
113
+ copy_match = true
114
+ elsif request.referent.metadata["volume"].to_i > svol.to_i and request.referent.metadata["volume"].to_i < evol.to_i
115
+ copy_match = true
116
+ end
117
+ end
118
+ end
119
+ end
120
+ if copy_match == true
121
+ request.add_service_response(:service=>self,:key=>holding.identifier.to_s,:value_string=>location.name,:value_alt_string=>item.call_number,:value_text=>item.status.to_s, :service_type_value => 'holding')
122
+ break
123
+ end
124
+ else
125
+ request.add_service_response(:service=>self,:key=>holding.identifier.to_s,:value_string=>location.name,:value_alt_string=>item.call_number,:value_text=>item.status.to_s,:service_type_value=>'holding')
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Check the leader to determine if this is defined as a conference or proceeding
133
+ # in the MARC
134
+ def is_conference?(marc)
135
+ # Check the leader/008 for books and serials
136
+ return true if marc['008'].value[29,1] == '1' && marc.leader[6,1].match(/[at]/) && marc.leader[7,1].match(/[abcdms]/)
137
+ # Check the leader/008 for scores and recordings
138
+ return true if marc['008'].value[30,2] == 'c' && marc.leader[6,1].match(/[cdij]/) && marc.leader[7,1].match(/[abcdms]/)
139
+ # Loop through the 006s
140
+ marc.find_all {|f| ('006') === f.tag}.each { | fxd_fld |
141
+ return true if fxd_fld.value[12,1] == '1' && fxd_fld.value[0,1].match(/[ats]{1}/)
142
+ return true if fxd_fld.value[13,2]== 'c' && fxd_fld.value[0,1].match(/[cdij]{1}/)
143
+ }
144
+ return false
145
+ end
146
+
147
+
148
+ # Determines the kind of record referred to in the MARC leader/fixed fields
149
+ # (Book, Serial, Map, etc.)
150
+ def record_type(marc)
151
+ type = marc.leader[6,1]
152
+ blvl = marc.leader[7,1]
153
+ valid_types = ['a','t','g','k','r','o','p','e','f','c','d','i','j','m']
154
+ rec_types = {
155
+ 'BKS' => { :type => /[at]{1}/, :blvl => /[acdm]{1}/ },
156
+ 'SER' => { :type => /[a]{1}/, :blvl => /[bs]{1}/ },
157
+ 'VIS' => { :type => /[gkro]{1}/, :blvl => /[abcdms]{1}/ },
158
+ 'MIX' => { :type => /[p]{1}/, :blvl => /[cd]{1}/ },
159
+ 'MAP' => { :type => /[ef]{1}/, :blvl => /[abcdms]{1}/ },
160
+ 'SCO' => { :type => /[cd]{1}/, :blvl => /[abcdms]{1}/ },
161
+ 'REC' => { :type => /[ij]{1}/, :blvl => /[abcdms]{1}/ },
162
+ 'COM' => { :type => /[m]{1}/, :blvl => /[abcdms]{1}/ }
163
+ }
164
+
165
+ rec_types.each_key do | rec_type |
166
+ return rec_type if type.match(rec_types[rec_type][:type]) and blvl.match(rec_types[rec_type][:blvl])
167
+ end
168
+ end
169
+
170
+ # When given a ServiceResponse object as its argument, returns the anchor text and URL
171
+ def to_fulltext(response)
172
+ return {:display_text=>response.response_key, :url=>response.value_string}
173
+ end
174
+
175
+ # When given a ServiceResponse object as its argument, returns the object as a hash
176
+ # with the keys:
177
+ # - :display_text
178
+ # - :call_number
179
+ # - :status
180
+ # - :source_name (which equates to self.display_name)
181
+ def to_holding(response)
182
+ return {:display_text=>response.value_string,:call_number=>response.value_alt_string,:status=>response.value_text,:source_name=>self.display_name}
183
+ end
184
+
185
+
186
+
187
+ # When given a MARC record, this method fills in any missing
188
+ # pieces of the Request.referent. 'accuracy' is a 3 point scale
189
+ # determined by the client, 1 = 'possibly the correct resource', 2 =
190
+ # same resource, but not necessarily the same edition, 3 = exactly the
191
+ # same resource
192
+ def enhance_referent(marc, request, accuracy)
193
+ return unless accuracy > 2
194
+
195
+ title_key = case request.referent.format
196
+ when "book" then "btitle"
197
+ when "journal" then "jtitle"
198
+ when "dissertation" then "title"
199
+ end
200
+ metadata = request.referent.metadata
201
+ unless metadata[title_key]
202
+ if request.referent.metadata["title"] && title_key != "title"
203
+ request.referent.enhance_referent(title_key, metadata["title"])
204
+ else
205
+ request.referent.enhance_referent(title_key, marc['245'].value)
206
+ end
207
+ end
208
+ unless metadata["au"]
209
+ if marc['100'] && marc['100']['a']
210
+ request.referent.enhance_referent('au', marc['100']['a'])
211
+ end
212
+ end
213
+ unless metadata["aucorp"]
214
+ if marc['110'] && marc['110']['a']
215
+ request.referent.enhance_referent('aucorp', marc['110']['a'])
216
+ end
217
+ end
218
+ return unless accuracy > 3
219
+ unless metadata["place"]
220
+ if marc['260'] && marc['260']['a']
221
+ request.referent.enhance_referent('place', marc['260']['a'])
222
+ end
223
+ end
224
+ unless metadata["pub"]
225
+ if marc['260'] && marc['260']['b']
226
+ request.referent.enhance_referent('pub', marc['260']['b'])
227
+ end
228
+ end
229
+ unless metadata["edition"]
230
+ if marc['250'] && marc['250']['a']
231
+ request.referent.enhance_referent('edition', marc['250']['a'])
232
+ end
233
+ end
234
+ unless metadata["series"]
235
+ if marc['490'] && marc['490']['a']
236
+ request.referent.enhance_referent('series', marc['490']['a'])
237
+ elsif marc['730'] && marc['730']['a']
238
+ request.referent.enhance_referent('series', marc['730']['a'])
239
+ end
240
+ end
241
+ unless metadata["date"] or request.referent.format == 'journal'
242
+ if marc['260'] && marc['260']['c']
243
+ request.referent.enhance_referent('date', marc['260']['c'])
244
+ end
245
+ end
246
+ unless request.referent.format
247
+ type = self.record_type(marc)
248
+ request.referent.enhance_referent('format', 'book', false) if type == "BKS"
249
+ request.referent.enhance_referent('format', 'journal', false) if type == "SER"
250
+ end
251
+ unless metadata["genre"]
252
+ if self.is_conference?(marc)
253
+ if metadata["atitle"]
254
+ request.referent.enhance_referent('genre', 'proceeding')
255
+ else
256
+ request.referent.enhance_referent('genre', 'conference')
257
+ end
258
+ elsif type = self.nature_of_contents(marc)
259
+ case type
260
+ when "dissertation" then request.referent.enhance_referent('format', 'dissertation', false)
261
+ when "patent" then request.referent.enhance_referent('format', 'patent', false)
262
+ when "report" then request.referent.enhance_referent('genre', 'report')
263
+ end
264
+ else
265
+ type = self.record_type(marc)
266
+ if type == "BKS"
267
+ request.referent.enhance_referent('format', 'book', false) unless request.referent.format == 'book'
268
+ if metadata["atitle"]
269
+ request.referent.enhance_referent('genre', 'bookpart')
270
+ else
271
+ request.referent.enhance_referent('genre', 'book')
272
+ end
273
+ elsif type == "SER"
274
+ request.referent.enhance_referent('format', 'journal', false) unless request.referent.format == 'journal'
275
+ if metadata["atitle"]
276
+ request.referent.enhance_referent('genre', 'article')
277
+ elsif metadata["issue"]
278
+ request.referent.enhance_referent('genre', 'issue')
279
+ else
280
+ request.referent.enhance_referent('genre', 'journal')
281
+ end
282
+ end
283
+ end
284
+ end
285
+
286
+ unless metadata["isbn"]
287
+ if marc['020'] && marc['020']['a']
288
+ request.referent.enhance_referent('isbn', marc['020']['a'])
289
+ end
290
+ end
291
+ unless metadata["issn"]
292
+ if marc['022'] && marc['022']['a']
293
+ request.referent.enhance_referent('issn', marc['022']['a'])
294
+ end
295
+ end
296
+ unless metadata["sici"]
297
+ if marc['024'] && marc['024'].indicator1 == "4"
298
+ request.referent.enhance_referent('sici', marc['024']['a'])
299
+ end
300
+ end
301
+
302
+ unless metadata["coden"]
303
+ if marc['030'] && marc['030']['a']
304
+ request.referent.enhance_referent('coden', marc['030']['a'])
305
+ end
306
+ end
307
+ end
308
+
309
+ # When given a MARC record, determines if the referent should be
310
+ # defined as a dissertation, report or patent.
311
+ def nature_of_contents(marc)
312
+ types = {'m'=>'dissertation','t'=>'report','j'=>'patent'}
313
+ idx = nil
314
+ if self.record_type(marc) == 'BKS'
315
+ idx = 24
316
+ len = 4
317
+ elsif self.record_type(marc) == 'SER'
318
+ idx = 25
319
+ len = 3
320
+ end
321
+ if idx
322
+ marc['008'].value[idx,len].split(//).each do | char |
323
+ return types[char] if types.keys.index(char)
324
+ end
325
+ end
326
+ marc.find_all {|f| ('006') === f.tag}.each do | fxd_fld |
327
+ idx = nil
328
+ if fxd_fld.value[0,1].match(/[at]{1}/)
329
+ idx = 7
330
+ len = 4
331
+ elsif fxd_fld.value[0,1].match('s')
332
+ idx = 8
333
+ len = 3
334
+ end
335
+ if idx
336
+ fxd_fld.value[idx,len].split(//).each do | char |
337
+ return types[char] if types.keys.index(char)
338
+ end
339
+ end
340
+ end
341
+ return false
342
+ end
343
+
344
+ # Builds a URL either based on a URL in the value_string or builds a link to the
345
+ # OPAC using the direct_link_arg and the bib ID.
346
+ def response_url(service_type, http_params)
347
+ response service_type.service_response
348
+ return CGI.unescapeHTML(response.value_string) if response.value_string.match(/^http(s)?:\/\//)
349
+ return @url+'&'+@direct_link_arg+response.response_key
350
+ end
351
+ end
@@ -0,0 +1,316 @@
1
+ # EXPERIMENTAL, uncomplete.
2
+ # Searches Open Library for fulltext, and cover images.
3
+ # To some extent duplicates what the InternetArchive service does,
4
+ # but using the OpenLibrary API.
5
+ #
6
+ # This service right now will only search on isbn/oclcnum/lccn identifiers,
7
+ # not on title/author keyword.
8
+ #
9
+ # Only a subset of OL cover images are actually available via API (those
10
+ # submitted by users). Here is an example: ?rft.isbn=0921307802
11
+ # Size of images returned is unpredictable. They can be huge sometimes.
12
+ # Counting on enforced re-sizing in img tag attributes.
13
+ #
14
+ #
15
+
16
+ class OpenLibrary < Service
17
+ require 'open-uri'
18
+ require 'json'
19
+ include MetadataHelper
20
+
21
+ attr_reader :url
22
+
23
+ def service_types_generated
24
+
25
+ types = Array.new
26
+ types.push( ServiceTypeValue[:fulltext]) if @get_fulltext
27
+ types.push( ServiceTypeValue[:highlighted_link]) if @get_fulltext
28
+ types.push( ServiceTypeValue[:cover_image]) if @get_covers
29
+
30
+ return types
31
+
32
+ # FIXME add these service types
33
+ #ServiceTypeValue[:table_of_contents]
34
+ #ServiceTypeValue[:search_inside]
35
+
36
+ end
37
+
38
+ def initialize(config)
39
+ @api_url = "http://openlibrary.org/api"
40
+ @display_name = "Open Library"
41
+ # in case the structure of an empty response changes
42
+ @empty_response = {"result" => [], "status" => "ok"}
43
+ @num_full_views = 1
44
+
45
+ # Can turn on and off each type of service
46
+ @get_fulltext = true
47
+ @get_covers = true
48
+ @enhance_metadata = true
49
+
50
+ # openlibrary goes straight to the flipbook; archive.org to main page
51
+ @fulltext_base_url = 'http://archive.org/details' #'http://openlibrary.org/details'
52
+ @download_link = true
53
+
54
+ @credits = {
55
+ "OpenLibrary" => "http://openlibrary.org/"
56
+ }
57
+
58
+ super(config)
59
+ end
60
+
61
+ def handle(request)
62
+ get_data(request)
63
+ return request.dispatched(self,true)
64
+ end
65
+
66
+ def get_data(request)
67
+ ids = get_identifiers(request.referent)
68
+ return nil if ids.blank?
69
+ ol_keys = do_id_query(ids)
70
+ return nil if ol_keys.blank?
71
+
72
+ editions = get_editions(ol_keys)
73
+ return nil if editions.blank?
74
+
75
+ enhance_metadata(request.referent, editions) if @enhance_metadata
76
+
77
+ add_cover_image(request, editions) if @get_cover_image
78
+
79
+ if ( @get_fulltext)
80
+ full_text_editions = select_fulltext(editions)
81
+ unless full_text_editions.blank?
82
+ create_fulltext_service_responses(request, full_text_editions)
83
+ create_download_link(request, full_text_editions) if @download_link
84
+ end
85
+ end
86
+
87
+ # Open Libary metadata looks messy right now and incomplete
88
+ # if there is only one edition returned then we return a highlighted link
89
+ # otherwise best to just leave it off
90
+ if editions.length == 1
91
+ # FIXME add this method
92
+ #create_highlighted_link(request, editions)
93
+ end
94
+
95
+ end
96
+
97
+ def get_identifiers(rft)
98
+ isbn = get_identifier(:urn, "isbn", rft)
99
+ oclcnum = get_identifier(:info, "oclcnum", rft)
100
+ lccn = get_identifier(:info, "lccn", rft)
101
+
102
+ h = {}
103
+ h['isbn'] = isbn unless isbn.blank?
104
+ h['oclcnum'] = oclcnum unless oclcnum.blank?
105
+ h['lccn'] = lccn unless lccn.blank?
106
+ return h
107
+ end
108
+
109
+ # only returns the unique keys from all the results
110
+ def do_id_query(ids)
111
+ responses = []
112
+ ids.each do |k, v|
113
+ new_key_value = map_key(k, v)
114
+ next if new_key_value.blank? #we probably have bad ISBN, could be bad key though
115
+ responses << get_thing(new_key_value)
116
+ end
117
+ selected = responses.map { |r| r['result'] }.flatten.compact.uniq
118
+ return selected
119
+ end
120
+
121
+ # given a hash as a query it returns a hash
122
+ def get_thing(query_hash)
123
+ query = {"type" => "/type/edition"}.merge(query_hash)
124
+ response = open(@api_url + "/things?query=" + CGI.escape(query.to_json) ).read
125
+ JSON.parse(response)
126
+ end
127
+
128
+ # Contacts OL and gets data records for editions/manifestations
129
+ # matching any of keys we have.
130
+ def get_editions(ol_keys)
131
+ editions = []
132
+ ol_keys.each do |k|
133
+ link = @api_url + "/get?key=" + k
134
+ resp = open(link).read
135
+ editions << JSON.parse(resp)['result']
136
+ end
137
+ return editions
138
+ end
139
+
140
+ def map_key(k, v)
141
+ new_key = case k
142
+ when "lccn" then "lccn"
143
+ when "oclcnum" then "oclc_numbers"
144
+ when "isbn"
145
+ if v.length == 10
146
+ "isbn_10"
147
+ elsif v.length == 13
148
+ "isbn_13"
149
+ end
150
+ end
151
+ return { new_key => v}
152
+ end
153
+
154
+ # right now we only know of a work having fulltext if it has an ocaid
155
+ # in case we discover other ways to determine fulltext availability we
156
+ # move it to its own method
157
+ def select_fulltext(editions)
158
+ editions.select do |ed|
159
+ ! ed['ocaid'].blank?
160
+ end
161
+ end
162
+
163
+ def create_fulltext_service_responses(request, editions)
164
+ count = 0
165
+ #note = @note
166
+ editions.each do |ed|
167
+ title = ed['title']
168
+ url = @fulltext_base_url + '/' +ed['ocaid']
169
+ request.add_service_response(
170
+ :service=>self,
171
+ :display_text=>@display_name,
172
+ :url=>url,
173
+ :notes=>title,
174
+ :service_type_value => :fulltext )
175
+
176
+ count += 1
177
+ break if count == @num_full_views
178
+ end
179
+ end
180
+
181
+ # TODO: If first one doesn't have a download, try second?
182
+ # In general, we need a better way of grouping ALL the results
183
+ # available for the user.
184
+ # Creates a highlighted_link for download of PDF
185
+ # for first edition listed.
186
+ def create_download_link(request, editions)
187
+ return nil unless editions
188
+ ed = editions[0] if editions.length
189
+ return nil unless ed['ocaid']
190
+ server = "www.archive.org"
191
+ pdf = "/download/"<< ed['ocaid'] << "/" <<
192
+ ed['ocaid'] << ".pdf"
193
+ url = "http://" << server << pdf
194
+
195
+ bytes = determine_download_size(server, pdf)
196
+ return nil if bytes.nil? || bytes == 0
197
+
198
+ note = bytes_to_mb(bytes)
199
+
200
+
201
+ request.add_service_response(
202
+ :service=>self,
203
+ :display_text=>"Download: " << ed['title'],
204
+ :url=>url,
205
+ :notes=> ("%.1f" % note) + " MB",
206
+ :service_type_value => :highlighted_link )
207
+ end
208
+
209
+ # they redirect so we actually have to do two HEAD requests to get the
210
+ # actual content length. Returns bytes as int.
211
+ def determine_download_size(server, pdf)
212
+ real_location = ''
213
+ Net::HTTP.start(server, 80) do |http|
214
+ # Send a HEAD request
215
+ response = http.head(pdf)
216
+ # Get the real location
217
+ real_location = response['Location']
218
+ end
219
+ m = real_location.match(/http:\/\/(.*?)(\/.*)/)
220
+ real_server = m[1]
221
+ real_pdf = m[2]
222
+ Net::HTTP.start(real_server, 80) do |http|
223
+ # Send a HEAD request
224
+ resp = http.head(real_pdf)
225
+
226
+ return nil if resp.kind_of?(Net::HTTPServerError) || resp.kind_of?(Net::HTTPClientError)
227
+
228
+ bytes = resp['Content-Length'].to_i
229
+ return bytes
230
+ end
231
+ end
232
+
233
+ def bytes_to_mb(bytes)
234
+ bytes / (1024.0 * 1024.0)
235
+ end
236
+
237
+ def add_cover_image(request, editions)
238
+ cover_image = find_coverimages(editions)
239
+ return nil if cover_image.blank?
240
+ #FIXME need to add other sizes
241
+ #FIXME correct @urls and use one of those
242
+ url = "http://openlibrary.org" + cover_image
243
+ request.add_service_response(
244
+ :service=>self,
245
+ :display_text => 'Cover Image',
246
+ :key=> 'medium',
247
+ :url => url,
248
+ :size => 'medium',
249
+ :service_type_value => :cover_image)
250
+ end
251
+
252
+ # pick the first of the coverimages found
253
+ def find_coverimages(editions)
254
+ images = editions.map{|ed| ed['coverimage']}.compact
255
+ # filter out fake ones
256
+ images.reject! { |url| url =~ /book\.trans\.gif$/ }
257
+ return images[0]
258
+ end
259
+
260
+ def enhance_metadata(referent, editions)
261
+ # Which one should we use to enhance? Whichever has the largest
262
+ # oclcnum, or if none of them have an oclcnum, then whichever
263
+ # has the most metadata elements.
264
+ winner = nil
265
+ winner_oclcnum = 0
266
+ winner_numfields = 0
267
+ editions.each do |e|
268
+ score = score_metadata(e)
269
+ if ( ( score[:oclcnum] && score[:oclcnum] > winner_oclcnum ) ||
270
+ ( winner_oclcnum == 0 && score[:numfields] > winner_numfields))
271
+ winner = e
272
+ winner_oclcnum = score[:oclcnum] if score[:oclcnum]
273
+ winner_numfields = score[:numfields]
274
+ end
275
+ end
276
+
277
+ if (winner)
278
+ referent.enhance_referent("title", winner["title"], true, false, {:overwrite=>false}) unless winner["title"].blank?
279
+
280
+ referent.enhance_referent("pub", winner["publishers"].join(","), true, false, {:overwrite=>false}) unless winner["publishers"].blank?
281
+
282
+ referent.enhance_referent("date", winner["publish_date"], true, false, {:overwrite=>false}) if winner["publish_date"] =~ /^\d\d\d\d$/
283
+
284
+ referent.enhance_referent("pub", winner["publish_places"].join(","), true, false, {:overwrite=>false}) unless winner["publish_places"].blank?
285
+
286
+ referent.enhance_referent("lccn", winner["lccn"][0], true, false, {:overwrite=>false}) unless winner["lccn"].blank?
287
+
288
+ # ISBN, prefer 13 if possible
289
+ referent.enhance_referent("isbn", winner["isbn_13"][0], true, false, {:overwrite=>false}) unless winner["isbn_13"].blank?
290
+
291
+ referent.enhance_referent("isbn", winner["isbn_10"][0], true, false, {:overwrite=>false}) if winner["isbn_13"].blank? && ! winner["isbn_10"].blank?
292
+
293
+ referent.enhance_referent("oclcnum", winner["oclc_numbers"][0], true, false, {:overwrite=>false}) unless winner["oclc_numbers"].blank?
294
+
295
+ end
296
+
297
+ end
298
+
299
+ # Score an edition in terms of how good it's metadata is.
300
+ # Returns a two-element array, first element is OCLCnum (or nil),
301
+ # second element is number of complete metadata elements.
302
+ # We like an OCLCnum, especially a higher one, and we like more
303
+ # elements.
304
+ def score_metadata(edition)
305
+ oclcnum = edition["oclc_numbers"].collect {|i| i.to_i}.max unless edition["oclc_numbers"].blank?
306
+ oclcnum = nil if oclcnum == 0
307
+
308
+ score = 0
309
+ ["title", "publish_places", "publishers", "publish_date", "isbn_10", "isbn_13", "lccn"].each do |key|
310
+ score = score + 1 unless edition[key].blank?
311
+ end
312
+
313
+ return {:oclcnum => oclcnum, :numfields => score}
314
+ end
315
+
316
+ end