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
data/lib/service.rb ADDED
@@ -0,0 +1,336 @@
1
+ # Services are defined from the config/umlaut_config/services.yml file.
2
+ # hey should have the following properties
3
+ # id : unique internal string for the service, unique in yml file
4
+ # display_name : user displayable string
5
+ # url : A base url of some kind used by specific service
6
+ # type : Class name of class found in lib/service_adaptors to be used for logic
7
+ # priority: 0-9 (foreground) or a-z (background) for order of service operation
8
+ #
9
+ # Specific service_adaptor classes may have specific addtional configuration,
10
+ # commonly including 'password' or 'api_key'.
11
+ # specific service can put " required_config_parms :param1, :param2"
12
+ # in definition, for requirement exception raising on initialize.
13
+ #
14
+ # = Service Sub-classes
15
+ # Can include required config params in the class definition, eg:
16
+ # required_config_params :api_key, :base_url
17
+ #
18
+ # Should define #service_types_generated returning an array of
19
+ # ServiceTypeValues. This is neccesary for the Service to be
20
+ # run as a background service, and have the auto background updater
21
+ # work.
22
+ #
23
+ # The vast majority of services are 'standard' services, however
24
+ # there are other 'tasks' that a service can be. Well, right now, one
25
+ # other, 'link_out_filter'. The services 'task' config property
26
+ # sets the task/function/hook of the service. Default is 'standard'.
27
+ #
28
+ # A standard service defines handle(request)
29
+ #
30
+ # A link_out_filter service defines link_out_filter(request, url). If service
31
+ # returns a new url from filter_url, that's the url the user will be directed
32
+ # to. If service returns original url or nil, original url will still be used.
33
+
34
+ class Service
35
+ attr_reader :priority, :service_id, :url, :task, :status, :name
36
+ attr_writer :session_id
37
+ attr_accessor :request
38
+ @@required_params_for_subclass = {} # initialize class var
39
+
40
+ # Some constants for 'function' values
41
+ StandardTask = 'standard'
42
+ LinkOutFilterTask = 'link_out_filter'
43
+
44
+
45
+ def initialize(config)
46
+
47
+ config.each do | key, val |
48
+ self.instance_variable_set(('@'+key).to_sym, val)
49
+ end
50
+
51
+ # task defaults to standard
52
+ @task ||= StandardTask
53
+
54
+ # check required params, and throw if neccesary
55
+
56
+ required_params = Array.new
57
+ # Some things required for all services
58
+ required_params << "priority"
59
+ # Custom things for this particular sub-class
60
+
61
+ required_params.concat( @@required_params_for_subclass[self.class.name] ) if @@required_params_for_subclass[self.class.name]
62
+ required_params.each do |param|
63
+ begin
64
+ value = self.instance_variable_get('@' + param.to_s)
65
+ # docs say it raises a nameerror if it doesn't exist, docs
66
+ # lie. So we'll just raise one ourselves, and catch it, to
67
+ # handle both cases.
68
+ raise NameError if value.nil?
69
+ rescue NameError
70
+ raise ArgumentError.new("Missing Service configuration parameter. Service type #{self.class} (id: #{self.id}) requires a config parameter named '#{param}'. Check your config/umlaut_config/services.yml file.")
71
+ end
72
+ end
73
+ end
74
+
75
+ # Must be implemented by concrete sub-class. return an Array of
76
+ # ServiceTypeValues constituting the types of ServiceResponses the service
77
+ # might generate. Used by Umlaut infrastructure including the background
78
+ # service execution scheme itslef, as well asxml services returning
79
+ # information on services in progress.
80
+ #
81
+ # Example for a service that only generates fulltext:
82
+ # return [ ServiceTypeValue[:fulltext] ]
83
+ def service_types_generated
84
+ raise Exception.new("#{self.class}: service_types_generated() must be implemented by Service concrete sub-class!")
85
+ end
86
+
87
+
88
+ # Method that should actually be called to trigger the service.
89
+ # Will check pre-emption.
90
+ def handle_wrapper(request)
91
+ unless ( preempted_by(request) )
92
+ return handle(request)
93
+ else
94
+ # Pre-empted, log and close dispatch record as 'succesful'.
95
+ Rails.logger.debug("Service #{service_id} was pre-empted and not run.")
96
+ return request.dispatched(self, true)
97
+ end
98
+ end
99
+
100
+ # Implemented by sub-class. Standard response-generating services implement
101
+ # this method to do their work, generate responses and/or metadata.
102
+ def handle(request)
103
+ raise Exception.new("#{self.class}: handle() must be implemented by Service concrete sub-class, for standard services!")
104
+ end
105
+
106
+ # This method is implemented by a concrete sub-class meant to
107
+ # fulfill the task:link_out_filter. Will be called when the user clicks
108
+ # on a url that will redirect external to Umlaut. The link_out_filter
109
+ # service has the ability to intervene and record and/or change
110
+ # the url. link_out_filters are called in order of priority config param
111
+ # assigned, 0 through 9.
112
+ #
113
+ # orig_url is the current url umlaut is planning on sending the user to.
114
+ # service_type is the ServiceType object responsible for this url.
115
+ # the third argument is reserved for future use an options hash.
116
+ def link_out_filter(orig_url, service_response, other_args = {})
117
+ raise Exception.new("#{self.class}: #link_out_filter must be implemented by Service concrete sub-class with task link_out_filter!")
118
+ end
119
+
120
+
121
+ def display_name
122
+ # If no display_name is set, default to the id string. Not a good idea,
123
+ # but hey.
124
+ return @display_name ||= self.service_id
125
+ end
126
+
127
+
128
+ # Pass this method a ServiceResponse object, it will return a hash-like object of
129
+ # display values, for the view. Implementation is usually in sub-class, by
130
+ # means of a set of methods "to_[service type name]" implemented in sub-class
131
+ #. parseResponse will find those. Subclasses will not generally override
132
+ # view_data_from_service_type, although they can for complete custom
133
+ # handling. Make sure to return a Hash or hash-like (duck-typed) object.
134
+ def view_data_from_service_type(service_response)
135
+
136
+ service_type_code = service_response.service_type_value.name
137
+
138
+ begin
139
+ # try to call a method named "to_#{service_type_code}", implemented by sub-class
140
+ self.send("to_#{service_type_code}", service_response)
141
+ rescue NoMethodError
142
+ # No to_#{response_type} method? How about the catch-all method?
143
+ # If not implemented in sub-class, we have a VERY basic
144
+ # default implementation in this class.
145
+ self.send("response_to_view_data", service_response)
146
+ end
147
+ end
148
+
149
+ # Default implementation to take a ServiceResponse and parse
150
+ # into a hash of values useful to the view. Basic implementation
151
+ # just asks ServiceResposne for it's data_values object, which
152
+ # contains all ServiceResponse data (including arbitrary keys serialized
153
+ # in the hash) in an object with the hash accessor method [] .
154
+ def response_to_view_data(service_response)
155
+ return service_response.data_values
156
+ end
157
+
158
+ # Sub-class can call class method like:
159
+ # required_config_params :symbol1, :symbol2, symbol3
160
+ # in class definition body. List of config parmas that
161
+ # are required, exception will be thrown if not present.
162
+ def self.required_config_params(*params)
163
+ params.each do |p|
164
+ # Key on name of specific sub-class. Since this is a class
165
+ # method, that should be self.name
166
+ @@required_params_for_subclass[self.name] ||= Array.new
167
+ a = @@required_params_for_subclass[self.name]
168
+ a.push( p ) unless a.include?( p )
169
+ end
170
+ end
171
+
172
+
173
+ def session_id
174
+ unless (@session_id)
175
+ raise Exception.new("This service has not been initialized with a request!") unless request
176
+
177
+ @session_id = request.session_id
178
+ end
179
+ return @session_id
180
+ end
181
+
182
+ # FakeSession little placeholder class we'll use in self.session to trick
183
+ # out Rails.
184
+ class FakeSession
185
+ attr_accessor :session_id
186
+ def initialize(a_session_id)
187
+ self.session_id = a_session_id
188
+ end
189
+ def new_session
190
+ return self.session_id
191
+ end
192
+ end
193
+ # Returns a read-only version of the session hash. Lazy loaded.
194
+ # See #update_session for making changes. Will have to be fixed
195
+ # for Rails 2.3.
196
+ def session
197
+ #lazy load
198
+ unless (@session_data || session_id.blank? )
199
+ # Craziness to restore a session in Rails pre 2.2. Will definitely
200
+ # need to be changed for Rails 2.3
201
+
202
+ fake_cgi_session = FakeSession.new(session_id)
203
+
204
+ @session_store_obj =
205
+ ActionController::Base.session_store.new(fake_cgi_session)
206
+ @_session_data = @session_store_obj.restore
207
+ # modifications here are not going to be automatically stored,
208
+ # so don't do them. See #update_session instead.
209
+ @session_data = @_session_data.clone
210
+ @session_data.freeze
211
+ end
212
+ if ( session_id.blank? && @session_data.nil?)
213
+ raise Exception.new("No session_id is available, therefore no session is available.")
214
+ end
215
+ return @session_data
216
+ end
217
+
218
+
219
+ # If we just allowed changes to our session hash, and rewrote it to
220
+ # the store, we'd get a race condition where we could over-write another
221
+ # service's changes. Rails itself is actually subject to that too.
222
+ # So, you tell us exactly which values you want to update, we'll
223
+ # refetch a fresh session, and save it with your changes.
224
+ #
225
+ # We're not actually guarding against the race condition, just
226
+ # fetching and storing quickly and hoping to miss it. FIXME.
227
+ #
228
+ # example: update_session( :new_value => "foo", :other => "bar")
229
+ #
230
+ # Will have to be fixed for Rails 2.3.
231
+ def update_session(new_values)
232
+ # force a new fetch
233
+ @session = nil
234
+ session
235
+ #and update that guy, with our mutable version
236
+ @_session_data.merge!(new_values)
237
+ @session_store_obj.close
238
+ # and update our cached copy to have the changes.
239
+ @session_data = @_session_data.clone
240
+ @session_data.freeze
241
+ end
242
+
243
+ # This method is called by Umlaut when user clicks on a service response.
244
+ # Default implementation here just returns response['url']. You can
245
+ # over-ride in a sub-class to provide custom implementation of on-demand
246
+ # url generation. Second argument is the http request params sent
247
+ # by the client, used for service types that take form submissions (eg
248
+ # search_inside).
249
+ # Should return a String url.
250
+ def response_url(service_response, submitted_params )
251
+ url = service_response[:url]
252
+ raise "No url provided by service response" if url.nil? || url.empty?
253
+ return url
254
+ end
255
+
256
+
257
+ # Pre-emption hashes specify a combination of existing responses or
258
+ # service executions that can pre-empt this service. Can specify
259
+ # a service, a response type (ServiceTypeValue), or a combination of both.
260
+ #
261
+ # service's preempted_by property can either be a single pre-emption hash,
262
+ # or an array of pre-emption hashes.
263
+ #
264
+ # Can also specify that pre-emption is only of a certain service type
265
+ # generated by self.
266
+ #
267
+ # The Service base class will enforce pre-emption and not even run
268
+ # a service at all *so long as self_type is nil or '*' *. If the pre-emption
269
+ # only applies to certain types generated by the service and not the entire
270
+ # execution of the service, the concrete service subclass must implement
271
+ # logic to do that. Calling the preempted method with the second argument
272
+ # set will be helpful in writing this logic.
273
+ #
274
+ # A preemption hash has string keys:
275
+ # existing_service: id of service that will pre-empt this service.
276
+ # If key does not exist or is "*", then not specified,
277
+ # any service. (existing_type will be specified).
278
+ # existing_type: ServiceTypeValue name that pre-empts this
279
+ # service. "+" means that the service specified
280
+ # in existing_service must have generated some
281
+ # response, but type does not matter. "*" means
282
+ # that the service specified in existing_service
283
+ # must have completed succesfully, but may not
284
+ # have generated any responses.
285
+ # self_type: If blank or "*", preemption applies to any running
286
+ # of this service at all. If set to a ServiceTypeValue
287
+ # name, pre-emption is only of certain types generated
288
+ # by this service.
289
+ def preempted_by(uml_request, for_type_generated=nil)
290
+ preempted_by = @preempted_by
291
+ return false if preempted_by.nil?
292
+ preempted_by = [preempted_by] unless preempted_by.kind_of?(Array)
293
+ preemption = nil
294
+
295
+ preempted_by.each do | hash |
296
+ service = hash["existing_service"] || "*"
297
+ other_type = hash["existing_type"] || "*"
298
+ self_type = hash["self_type"] || "*"
299
+
300
+ next unless (self_type == "*" || self_type == for_type_generated)
301
+
302
+ if (other_type == "*")
303
+ # Need to check dispatched services instead of service_types,
304
+ # as we pre-empt even if no services created.
305
+ preemption =
306
+ uml_request.dispatched_services.to_a.find do |disp|
307
+ service == "*" ||
308
+ (disp.service_id == service &&
309
+ (disp.status == DispatchedService.Succesful ))
310
+ end
311
+ else
312
+ # Check service responses
313
+ preemption =
314
+ uml_request.service_responses.to_a.find do |response|
315
+ ( other_type == "*" || other_type == "+" ||
316
+ response.service_type_value.name == other_type) &&
317
+ ( service == "*" ||
318
+ response.service_id == service)
319
+ end
320
+ end
321
+ break if preemption
322
+ end
323
+ return (! preemption.nil? )
324
+ end
325
+
326
+ # used by render_service_credits helper method, returns
327
+ # a hash with keys being a human-displayable name of a third party
328
+ # to give 'credit' to, and value being a URL (or nil) to link the
329
+ # name to.
330
+ # computed from @credits config variable, or returns empty hash.
331
+ def credits
332
+ @credits || {}
333
+ end
334
+
335
+
336
+ end
@@ -0,0 +1,37 @@
1
+ # This is an abstract superclass other services over-ride to get
2
+ # extra ajaxy windows upon click on link.
3
+ class AjaxExport < Service
4
+ required_config_params :ajax_id, :controller
5
+
6
+ def initialize(config)
7
+ super(config)
8
+ end
9
+
10
+ # Standard method, used by background service updater. See Service docs.
11
+ def service_types_generated
12
+ types = [ ServiceTypeValue[:export_citation] ]
13
+
14
+ return types
15
+ end
16
+
17
+ def handle(request)
18
+
19
+ request.add_service_response(:service=>self,
20
+ :display_text => @display_text,
21
+ :link_supports_ajax_call => true,
22
+ :notes=> @note,
23
+ :service_type_value => 'export_citation' )
24
+
25
+ return request.dispatched(self, true)
26
+ end
27
+
28
+ def response_url(service_response, params)
29
+ # Hash that caller will pass to url_for to create an internally
30
+ # facing link.
31
+ return {:controller=>@form_controller,
32
+ :action=>@form_action,
33
+ :id => service_response.id,
34
+ :format => params[:format]}
35
+ end
36
+
37
+ end
@@ -0,0 +1,412 @@
1
+ #
2
+ # As of Aug 15 2009, Amazon API calls will require a secret key from Amazon.
3
+ # If you are unable to get such an account/key, this service can still be used
4
+ # for certain functions by setting 'make_aws_call' to false, and configuring
5
+ # 'service_types' to only include one or more of: ["search_inside",
6
+ # "highlighted_link", "excerpts"]
7
+ # Other services, such as enhance_referent and cover_image require api access.
8
+ #
9
+ # More about registering for and finding your AWS access key and secret key
10
+ # can be found here:
11
+ # http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/AWSCredentials.html
12
+ # http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/ViewingCredentials.html
13
+ #
14
+ #
15
+ # services.yml params:
16
+ # api_key: required. AWS "access key".
17
+ # secret_key: required unless make_aws_call==false. AWS "secret access key".
18
+ # associate_tag: required unless make_aws_call==false. Now required by Amazon API.
19
+ # sign up for an associates_id at: https://affiliate-program.amazon.com/
20
+ # it's the same thing as your 'associate id'.
21
+ # service_types: Optional. Array of strings of service type values to be
22
+ # loaded, to over-ride defaults.
23
+ # make_aws_call: default true. If false, then either uses an ASIN stored
24
+ # in referent from previous service, or tries assuming
25
+ # that an ISBN, if present, is the ASIN.
26
+ # of services ran an amazon service adaptor
27
+ # Can be used to split amazon into
28
+ # two waves, since highlighted_link and cover_image
29
+ # calls require _another_ HTTP request to amazon
30
+ # and screen scrape. Or can be used if you don't
31
+ # have access to Amazon API.
32
+ #
33
+ # See example of two-wave amazon in config/umlaut_distribution/services.yml-dist.
34
+ #
35
+ class Amazon < Service
36
+ require 'open-uri'
37
+ require 'nokogiri'
38
+ require 'isbn'
39
+
40
+
41
+ include MetadataHelper
42
+
43
+ required_config_params :url, :api_key, :associate_tag
44
+ attr_reader :url
45
+
46
+ def initialize(config)
47
+ # defaults
48
+ @url = 'http://webservices.amazon.com/onca/xml'
49
+ @reader_base_url = 'http://www.amazon.com/gp/reader/'
50
+ # Old version non-lightboxed, whcih doesn't work very well anymore.
51
+ # @reader_base_url = 'http://www.amazon.com/gp/sitbv3/reader/'
52
+ @display_name = "Amazon.com"
53
+ @display_text = "Amazon's page"
54
+ @service_types = ["abstract", "highlighted_link", "cover_image", "search_inside", "referent_enhance", "excerpts"]
55
+ @make_aws_call = true
56
+ @http_timeout = 5
57
+
58
+ @credits = {
59
+ "Amazon" => "http://www.amazon.com/"
60
+ }
61
+
62
+ super(config)
63
+
64
+ # Need the secret_key after 15 aug 09.
65
+ unless (@secret_key || ! @make_aws_call)
66
+ if ( Time.now < Time.gm(2009, 8, 15))
67
+ Rails.logger.warn("Amazon service will require a secret_key after 15 August 2009 to make Amazon API calls.")
68
+ else
69
+ raise Exception.new("Amazon API now requires a secret_key. The Amazon service can only be used with make_aws_call=false unless you have an Amazon secret key configured.")
70
+ end
71
+ end
72
+
73
+ # Only a few service types can get by without an aws call
74
+ if (! @make_aws_call &&
75
+ @service_types.find {|type| ! ["search_inside", "highlighted_link", "excerpts"].include?(type) } )
76
+ raise Exception.new("You can only set make_aws_call == false on the definition of an Amazon service adaptor when the adaptor is also set to generate no service responses other than highlighted_link, search_inside, and excerpts")
77
+ end
78
+ end
79
+
80
+
81
+ def service_types_generated
82
+ types = Array.new
83
+
84
+ @service_types.each do |type|
85
+ types.push( ServiceTypeValue[type])
86
+ end
87
+
88
+ return types
89
+ end
90
+
91
+ def handle(request)
92
+
93
+ isbn = request.referent.metadata['isbn']
94
+ isbn = isbn.gsub(/[^0-9X]/,'') if isbn
95
+
96
+ return request.dispatched(self, true) if isbn.blank?
97
+
98
+ begin
99
+
100
+ selected_aws_vals = {}
101
+ if ( @make_aws_call )
102
+ aws_response = make_aws_request( isbn )
103
+
104
+ return request.dispatched(self, true) if aws_response.blank?
105
+
106
+ # Add service responses based on AWS response
107
+ selected_aws_vals =
108
+ self.add_aws_service_responses(request, aws_response)
109
+ end
110
+
111
+ if ( selected_aws_vals == nil)
112
+ # no aws found.
113
+ return request.dispatched(self, true)
114
+ end
115
+
116
+ # Add service responses based on ASIN--may be run in a
117
+ # later service wave. Look up asin in db if we don't have
118
+ # it from current service exec.
119
+ asin = selected_aws_vals[:asin] ||
120
+ get_identifier(:urn, "asin", request.referent)
121
+
122
+ self.add_asin_service_responses(request, asin, selected_aws_vals[:item_url])
123
+
124
+ rescue TimeoutError
125
+ # Try again later if we timeout; temporary error condition.
126
+ return request.dispatched(self, DispatchedService::FailedTemporary)
127
+ rescue Exception => e
128
+ # Unexpected error, fatal error condition.
129
+ return request.dispatched(self, DispatchedService::FailedFatal, e)
130
+ end
131
+
132
+ return request.dispatched(self, true)
133
+ end
134
+
135
+ def make_aws_request(isbn)
136
+ # We're assuming the ISBN is the ASIN Amazon ID. Not neccesarily valid
137
+ # assumption, but works enough of the time and there's no easy
138
+ # alternative.
139
+ # Convert 13 to 10 if neccesary.
140
+
141
+ # got to try converting to 10. An ISBN-13 is never an ASIN.
142
+ isbn = ISBN.ten( isbn )
143
+
144
+
145
+ query_params = {
146
+ "Service"=>"AWSECommerceService",
147
+ "AWSAccessKeyId"=>@api_key,
148
+ "AssociateTag"=>@associate_tag,
149
+ "Operation"=>"ItemLookup",
150
+ "ResponseGroup"=>"Large",
151
+ "ItemId"=>isbn }
152
+
153
+ # has to be signed
154
+ query = nil
155
+
156
+ if ( @secret_key )
157
+ aws = AwsProductSign.new(:access_key => @api_key,
158
+ :secret_key => @secret_key )
159
+ query = aws.query_with_signature( query_params )
160
+ else
161
+ query = query_params.collect {|key, value| CGI.escape(key) + '=' + CGI.escape(value)}.join("&")
162
+ end
163
+
164
+ uri = URI.parse(self.url+'?'+query)
165
+ # send the request
166
+ http = Net::HTTP.new(uri.host, 80)
167
+ http.open_timeout = @http_timeout
168
+ http.read_timeout = @http_timeout
169
+ http_response = http.send_request('GET', uri.path + '?' + uri.query)
170
+
171
+ return http_response
172
+ end
173
+
174
+ def add_aws_service_responses(request, aws_response)
175
+ return_hash = Hash.new
176
+
177
+ aws = Nokogiri::XML(aws_response.body)
178
+ # extract and collect info from the xml
179
+
180
+ # if we get an error from Amazon, return now.
181
+ err = (aws.at("ItemLookupResponse/Items/Request/Errors/Error"))
182
+ err = (aws.at("ItemLookupErrorResponse")) if err.blank?
183
+
184
+ unless (err.blank?)
185
+ if (err.at('code').inner_text == 'AWS.InvalidParameterValue')
186
+ # Indicates an ISBN that Amazon doesn't know about, or that
187
+ # was mal-formed. We can't tell the difference, so either
188
+ # way let's silently ignore.
189
+ return
190
+ else
191
+ raise Exception.new("Error from Amazon web service: " + err.to_s)
192
+ end
193
+ end
194
+
195
+ asin = (aws.at("ItemLookupResponse/Items/Item/ASIN")).inner_text
196
+
197
+ # Store the asin in the referent as non-metadata private data, so
198
+ # a future background service can use it. Store as a urn identifier.
199
+ request.referent.add_identifier("urn:asin:#{asin}") unless asin.blank?
200
+
201
+ return_hash[:asin] = asin
202
+
203
+ if ( @service_types.include?("cover_image") )
204
+ # collect cover art urls
205
+ ["small","medium","large"].each do | size |
206
+ if (img = aws.at("ItemLookupResponse/Items/Item/"+size.capitalize+"Image/URL"))
207
+ request.add_service_response(
208
+ :service=>self,
209
+ :display_text => 'Cover Image',
210
+ :key=>size,
211
+ :url => img.inner_text,
212
+ :asin => asin,
213
+ :size => size,
214
+ :service_type_value => :cover_image)
215
+ end
216
+ end
217
+
218
+ end
219
+
220
+ item_url = (aws.at("ItemLookupResponse/Items/Item/DetailPageURL")).inner_text
221
+ # Store to return to caller
222
+ return_hash[:item_url] = item_url
223
+
224
+
225
+ # get description
226
+ if ( @service_types.include?("abstract") &&
227
+ desc =
228
+ (aws.at("ItemLookupResponse/Items/Item/EditorialReviews/EditorialReview/Content")))
229
+
230
+ # For some reason we need to un-escape the desc. Don't entirely get it.
231
+ desc_text = CGI.unescapeHTML( desc.inner_text )
232
+
233
+ unless ( desc_text.blank? )
234
+ request.add_service_response(
235
+ :service=>self,
236
+ :display_text => "Description from Amazon.com",
237
+ :url => item_url,
238
+ :key=>'abstract',
239
+ :value_string=>asin,
240
+ :content=>desc_text ,
241
+ :service_type_value => 'abstract')
242
+ end
243
+ end
244
+
245
+
246
+ if ( @service_types.include?("similar_item"))
247
+ # Get Amazon's 'similar products' to help recommend other useful items
248
+ (aws.search("ItemLookupResponse/Items/Item/SimilarProducts/SimilarProduct")).each do |similar|
249
+ request.add_service_response(
250
+ :service=>self,
251
+ :key=>'book',
252
+ :value_string=>(similar.at("ASIN")).inner_text,
253
+ :value_alt_string=>(similar.at("Title")).inner_text,
254
+ :service_type_value => 'similar_item')
255
+ end
256
+
257
+ end
258
+
259
+ if ( @service_types.include?("referent_enhance"))
260
+ item_attributes = aws.at("ItemLookupResponse/Items/Item/ItemAttributes")
261
+
262
+ request.referent.enhance_referent('format', 'book', false) unless request.referent.format == 'book'
263
+ metadata = request.referent.metadata
264
+ unless (metadata['btitle'] || metadata['title'])
265
+ if title = (item_attributes.at("Title"))
266
+ request.referent.enhance_referent('btitle', normalize_aws_title(title.inner_text))
267
+ end
268
+
269
+ end
270
+ # Enhance with full author name string even if aulast is already present, because full string may be useful for worldcat identities.
271
+ unless (metadata['au'] )
272
+ if author = (item_attributes.at("Author"))
273
+ request.referent.enhance_referent('au', author.inner_text)
274
+ end
275
+ end
276
+
277
+ unless metadata['pub']
278
+ if pub = (item_attributes.at("Publisher"))
279
+ request.referent.enhance_referent('pub', pub.inner_text)
280
+ end
281
+ end
282
+ unless metadata['tpages']
283
+ if tpages = (item_attributes.at("NumberOfPages"))
284
+ request.referent.enhance_referent('tpages', tpages.inner_text)
285
+ end
286
+ end
287
+ end
288
+
289
+ return return_hash
290
+ end
291
+
292
+ def add_asin_service_responses(request, asin, item_url)
293
+ # we want to highlight Amazon to link to 'search in this book', etc.
294
+ if asin
295
+ # Search or Look inside the book offered? We only know by trying and
296
+ # then screen-scraping.
297
+ search_inside = false
298
+ look_inside = false
299
+
300
+ # Check for search_inside or look_inside if we're configured
301
+ # to supply highlighted_link or search_inside, that's what we
302
+ # need it for.
303
+ if ( @service_types.include?("highlighted_link") ||
304
+ @service_types.include?("search_inside"))
305
+ inside_base = @reader_base_url + asin
306
+ # lame screen-scrape for search inside availability. We need to
307
+ # distinguish between no results, "look inside", and "search inside".
308
+ response = open(inside_base).read
309
+
310
+ # This regexp only suitable for screen-scraping the old-style "sitbv3"
311
+ # reader page screen
312
+ if (response.include?("<div class='sitb-pop-search'>"))
313
+ # then we have search_inside. I think this always includes 'look', but
314
+ # we'll test seperate for that.
315
+ search_inside= true
316
+ end
317
+
318
+ if (response.include?('<a href="/gp/reader/'))
319
+ # then we have look inside, not neccesarily search.
320
+ look_inside = true
321
+ end
322
+ end
323
+
324
+ if ( @service_types.include?("search_inside") && search_inside )
325
+ request.add_service_response(
326
+ :service => self,
327
+ :display_text=>@display_name,
328
+ :url=> inside_base,
329
+ :service_type_value => :search_inside
330
+ )
331
+ end
332
+
333
+ # Link to look inside if we have it, otherwise ordinary amazon detail
334
+ # page.
335
+
336
+ if (@service_types.include?("excerpts") &&
337
+ ( search_inside || look_inside ))
338
+
339
+
340
+ request.add_service_response(
341
+ :service=>self,
342
+ :url => inside_base,
343
+ :asin=>asin,
344
+ :display_text => @display_name,
345
+ :service_type_value => 'excerpts')
346
+
347
+ elsif ( @service_types.include?("highlighted_link"))
348
+ # Just link to Amazon page if we can. If we did the AWS request
349
+ # before, afraid we didn't store the item url, just the
350
+ # asin, reconstruct a valid one, even if not the one given to us
351
+ # by AWS.
352
+ amazon_page = item_url || ("http://www.amazon.com/o/ASIN/" + asin)
353
+
354
+
355
+ request.add_service_response(
356
+ :service=>self,
357
+ :url => amazon_page,
358
+ :asin=>asin,
359
+ :display_text => @display_text,
360
+ :service_type_value => 'highlighted_link')
361
+ end
362
+
363
+ end
364
+
365
+ end
366
+
367
+ # Catch url_for call for search_inside, because we're going to redirect
368
+ def response_url(service_response, submitted_params)
369
+ if ( ! (service_response.service_type_value.name == "search_inside" ))
370
+ return super(service_response, submitted_params)
371
+ else
372
+ # search inside!
373
+ base = service_response[:url]
374
+ query = CGI.escape(submitted_params["query"] || "")
375
+ url = base + "/ref=sib_dp_srch_pop?v=search-inside&keywords=#{query}&go=Go%21"
376
+ return url
377
+ end
378
+ end
379
+
380
+ #amazon is in the habit of including things in parens at the end
381
+ #of the title that aren't really part of the title. The parens
382
+ # are really an edition and/or series statement. We have nowhere
383
+ # good to store that.
384
+ def normalize_aws_title(title)
385
+ title.sub(/\([^)]*\)\s*$/, '')
386
+ end
387
+
388
+
389
+ end
390
+
391
+ # Example of no look or search:
392
+ # www.amazon.com/gp/reader/0794521789
393
+
394
+ # Example of look and search:
395
+ # http://www.amazon.com/gp/reader/1851960511
396
+
397
+ # Example of 'look inside' with no search:
398
+ # http://www.amazon.com/gp/reader/0140441115/
399
+
400
+ # Scraping /gp/reader page.
401
+
402
+ # Only look inside (preview) if link like:
403
+ # <a href="/gp/reader/1851960511/ref=sib_dp_pop_sup?ie=UTF8&amp;p=random#reader-link
404
+ # OR:
405
+ # <a href="/gp/reader/0140441115/ref=sib_dp_kd#reader-link" onclick="if (typeof(SitbReader) != 'undefined')
406
+
407
+
408
+ # Only search inside if:
409
+
410
+ # '<td class="tinypopup">Search Inside This Book:</td>'
411
+ # OR: <div class='sitb-pop-search'>
412
+