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.
- data/LICENSE +7 -0
- data/README.md +49 -0
- data/Rakefile +37 -0
- data/app/assets/images/error.gif +0 -0
- data/app/assets/images/export_bg_bot.gif +0 -0
- data/app/assets/images/export_bg_mid.gif +0 -0
- data/app/assets/images/export_bg_top.gif +0 -0
- data/app/assets/images/famfamfam/book_open.png +0 -0
- data/app/assets/images/famfamfam/cross.png +0 -0
- data/app/assets/images/famfamfam/page_sound.gif +0 -0
- data/app/assets/images/famfamfam/page_text.gif +0 -0
- data/app/assets/images/famfamfam/page_up.gif +0 -0
- data/app/assets/images/famfamfam/page_white.png +0 -0
- data/app/assets/images/famfamfam/readme.html +1495 -0
- data/app/assets/images/famfamfam/tiny_cross.png +0 -0
- data/app/assets/images/frame_remove.gif +0 -0
- data/app/assets/images/ico_go.gif +0 -0
- data/app/assets/images/jhu_findit.gif +0 -0
- data/app/assets/images/list_closed.png +0 -0
- data/app/assets/images/list_open.png +0 -0
- data/app/assets/images/more_info.gif +0 -0
- data/app/assets/images/rails.png +0 -0
- data/app/assets/images/request.gif +0 -0
- data/app/assets/images/spinner.gif +0 -0
- data/app/assets/javascripts/umlaut/ajax_windows.js +35 -0
- data/app/assets/javascripts/umlaut/ensure_window_size.js.erb +34 -0
- data/app/assets/javascripts/umlaut/expand_contract_toggle.js +25 -0
- data/app/assets/javascripts/umlaut/search_autocomplete.js +46 -0
- data/app/assets/javascripts/umlaut/simple_visible_toggle.js +8 -0
- data/app/assets/javascripts/umlaut/update_html.js +152 -0
- data/app/assets/javascripts/umlaut.js +17 -0
- data/app/assets/stylesheets/umlaut.css +857 -0
- data/app/controllers/application_controller.rb +14 -0
- data/app/controllers/export_email_controller.rb +123 -0
- data/app/controllers/js_helper_controller.rb +10 -0
- data/app/controllers/link_router_controller.rb +87 -0
- data/app/controllers/open_search_controller.rb +9 -0
- data/app/controllers/resolve_controller.rb +288 -0
- data/app/controllers/resource_controller.rb +83 -0
- data/app/controllers/search_controller.rb +328 -0
- data/app/controllers/search_methods/sfx3.rb +148 -0
- data/app/controllers/search_methods/sfx4.rb +257 -0
- data/app/controllers/search_methods/sfx_api.rb +47 -0
- data/app/controllers/store_controller.rb +64 -0
- data/app/controllers/umlaut/controller_behavior.rb +20 -0
- data/app/controllers/umlaut/controller_logic.rb +96 -0
- data/app/controllers/umlaut/error_handling.rb +48 -0
- data/app/controllers/umlaut_controller.rb +112 -0
- data/app/helpers/application_helper.rb +4 -0
- data/app/helpers/emailer_helper.rb +43 -0
- data/app/helpers/export_email_helper.rb +34 -0
- data/app/helpers/open_search_helper.rb +7 -0
- data/app/helpers/resolve_helper.rb +225 -0
- data/app/helpers/search_helper.rb +50 -0
- data/app/helpers/umlaut/footer_helper.rb +64 -0
- data/app/helpers/umlaut/helper.rb +62 -0
- data/app/helpers/umlaut/html_head_helper.rb +37 -0
- data/app/helpers/umlaut/url_generation.rb +77 -0
- data/app/mailers/emailer.rb +48 -0
- data/app/models/clickthrough.rb +2 -0
- data/app/models/collection.rb +259 -0
- data/app/models/crossref_lookup.rb +2 -0
- data/app/models/dispatched_service.rb +58 -0
- data/app/models/permalink.rb +29 -0
- data/app/models/referent.rb +473 -0
- data/app/models/referent_value.rb +14 -0
- data/app/models/request.rb +449 -0
- data/app/models/service_response.rb +179 -0
- data/app/models/service_store.rb +59 -0
- data/app/models/service_type_value.rb +58 -0
- data/app/models/service_wave.rb +150 -0
- data/app/models/sfx_db/az_additional_title.rb +11 -0
- data/app/models/sfx_db/az_letter_group.rb +11 -0
- data/app/models/sfx_db/az_title.rb +38 -0
- data/app/models/sfx_db/az_title_v2.rb +34 -0
- data/app/models/sfx_db/isbn.rb +12 -0
- data/app/models/sfx_db/issn.rb +12 -0
- data/app/models/sfx_db/object.rb +35 -0
- data/app/models/sfx_db/object_portfolio.rb +6 -0
- data/app/models/sfx_db/publisher.rb +10 -0
- data/app/models/sfx_db/sfx_db_base.rb +54 -0
- data/app/models/sfx_db/target.rb +9 -0
- data/app/models/sfx_db/target_service.rb +10 -0
- data/app/models/sfx_db/title.rb +10 -0
- data/app/models/sfx_db.rb +10 -0
- data/app/models/sfx_url.rb +35 -0
- data/app/views/emailer/citation.text.erb +28 -0
- data/app/views/emailer/short_citation.text.erb +8 -0
- data/app/views/export_email/_email.html.erb +25 -0
- data/app/views/export_email/_send_email.html.erb +3 -0
- data/app/views/export_email/_send_txt.html.erb +3 -0
- data/app/views/export_email/_txt.html.erb +62 -0
- data/app/views/export_email/email.html.erb +3 -0
- data/app/views/export_email/send_email.html.erb +1 -0
- data/app/views/export_email/send_txt.html.erb +1 -0
- data/app/views/export_email/txt.html.erb +3 -0
- data/app/views/js_helper/loader.erb.js +13 -0
- data/app/views/layouts/umlaut.html.erb +52 -0
- data/app/views/open_search/index.html.erb +9 -0
- data/app/views/resolve/_api_in_progress.xml.erb +21 -0
- data/app/views/resolve/_background_progress.html.erb +51 -0
- data/app/views/resolve/_background_updater.html.erb +38 -0
- data/app/views/resolve/_citation.html.erb +87 -0
- data/app/views/resolve/_coins.html.erb +1 -0
- data/app/views/resolve/_compact_citation.html.erb +33 -0
- data/app/views/resolve/_cover_image.html.erb +35 -0
- data/app/views/resolve/_fulltext.html.erb +55 -0
- data/app/views/resolve/_help.html.erb +17 -0
- data/app/views/resolve/_holding.html.erb +91 -0
- data/app/views/resolve/_related_items.html.erb +35 -0
- data/app/views/resolve/_search_inside.html.erb +62 -0
- data/app/views/resolve/_section_display.html.erb +49 -0
- data/app/views/resolve/_service_errors.html.erb +29 -0
- data/app/views/resolve/_standard_response_item.html.erb +89 -0
- data/app/views/resolve/api.xml.builder +72 -0
- data/app/views/resolve/background_status.html.erb +26 -0
- data/app/views/resolve/index.html.erb +73 -0
- data/app/views/resolve/partial_html_sections.xml.erb +30 -0
- data/app/views/search/_a_to_z.html.erb +6 -0
- data/app/views/search/_citation.html.erb +94 -0
- data/app/views/search/_pager.html.erb +60 -0
- data/app/views/search/books.html.erb +103 -0
- data/app/views/search/journal_search.html.erb +90 -0
- data/app/views/search/journals.html.erb +167 -0
- data/app/views/search/opensearch_description.rxml +10 -0
- data/app/views/testing/index.html.erb +1 -0
- data/app/views/umlaut/README +5 -0
- data/app/views/umlaut/error.html.erb +45 -0
- data/db/migrate/01_umlaut_init.rb +113 -0
- data/db/orig_fixed_data/service_type_values.yml +120 -0
- data/db/seeds.rb +7 -0
- data/lib/CronTab.rb +192 -0
- data/lib/aws_product_sign.rb +146 -0
- data/lib/exlibris/aleph/patron.rb +64 -0
- data/lib/exlibris/aleph/record.rb +54 -0
- data/lib/exlibris/aleph/rest_api.rb +29 -0
- data/lib/exlibris/primo/holding.rb +192 -0
- data/lib/exlibris/primo/rsrc.rb +17 -0
- data/lib/exlibris/primo/searcher.rb +276 -0
- data/lib/exlibris/primo/source/aleph.rb +46 -0
- data/lib/exlibris/primo/source/distribution/nyu_aleph.rb +323 -0
- data/lib/exlibris/primo/toc.rb +17 -0
- data/lib/exlibris/primo_ws.rb +140 -0
- data/lib/generators/templates/umlaut_services.yml +237 -0
- data/lib/generators/umlaut/asset_hooks_generator.rb +44 -0
- data/lib/generators/umlaut/install_generator.rb +110 -0
- data/lib/hip3/bib.rb +291 -0
- data/lib/hip3/bib_searcher.rb +302 -0
- data/lib/hip3/custom_field_lookup.rb +44 -0
- data/lib/hip3/holding.rb +50 -0
- data/lib/hip3/item.rb +65 -0
- data/lib/hip3/receipt.rb +7 -0
- data/lib/hip3/serial_copy.rb +82 -0
- data/lib/holding.rb +32 -0
- data/lib/marc_helper.rb +254 -0
- data/lib/metadata_helper.rb +312 -0
- data/lib/opensearch_feed.rb +398 -0
- data/lib/opensearch_query.rb +98 -0
- data/lib/referent_filter.rb +16 -0
- data/lib/referent_filters/dissertation_catch.rb +45 -0
- data/lib/section_renderer.rb +503 -0
- data/lib/service.rb +336 -0
- data/lib/service_adaptors/ajax_export.rb +37 -0
- data/lib/service_adaptors/amazon.rb +412 -0
- data/lib/service_adaptors/blacklight.rb +327 -0
- data/lib/service_adaptors/book_finder.rb +40 -0
- data/lib/service_adaptors/bx.rb +51 -0
- data/lib/service_adaptors/cover_thing.rb +73 -0
- data/lib/service_adaptors/elsevier_cover.rb +57 -0
- data/lib/service_adaptors/email_export.rb +10 -0
- data/lib/service_adaptors/ezproxy.rb +171 -0
- data/lib/service_adaptors/google_book_search.rb +442 -0
- data/lib/service_adaptors/gpo.rb +124 -0
- data/lib/service_adaptors/hathi_trust.rb +308 -0
- data/lib/service_adaptors/hip3_service.rb +150 -0
- data/lib/service_adaptors/hip_holding_search.rb +237 -0
- data/lib/service_adaptors/internet_archive.rb +488 -0
- data/lib/service_adaptors/isbn_db.rb +86 -0
- data/lib/service_adaptors/isi.rb +258 -0
- data/lib/service_adaptors/jcr.rb +146 -0
- data/lib/service_adaptors/opac.rb +351 -0
- data/lib/service_adaptors/open_library.rb +316 -0
- data/lib/service_adaptors/open_library_cover.rb +73 -0
- data/lib/service_adaptors/primo_service.rb +392 -0
- data/lib/service_adaptors/primo_source.rb +78 -0
- data/lib/service_adaptors/pubmed.rb +133 -0
- data/lib/service_adaptors/request_to_fixture.rb +68 -0
- data/lib/service_adaptors/scopus.rb +295 -0
- data/lib/service_adaptors/sfx-new.rb +557 -0
- data/lib/service_adaptors/sfx.rb +566 -0
- data/lib/service_adaptors/sfx_backchannel_record.rb +69 -0
- data/lib/service_adaptors/txt_holding_export.rb +32 -0
- data/lib/service_adaptors/ulrichs_cover.rb +57 -0
- data/lib/service_adaptors/ulrichs_link.rb +47 -0
- data/lib/service_adaptors/worldcat.rb +116 -0
- data/lib/service_adaptors/worldcat_identities.rb +591 -0
- data/lib/tasks/umlaut.rake +134 -0
- data/lib/umlaut/default_configuration.rb +5 -0
- data/lib/umlaut/routes.rb +136 -0
- data/lib/umlaut/version.rb +3 -0
- data/lib/umlaut.rb +37 -0
- data/lib/umlaut_configurable.rb +343 -0
- data/lib/umlaut_http.rb +100 -0
- data/lib/xml_schema_helper.rb +109 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/umlaut_controller.rb +112 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database-jhu.yml +44 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +34 -0
- data/test/dummy/config/environments/production.rb +60 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +61 -0
- data/test/dummy/config/umlaut_services.yml +237 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/migrate/20111228211210_umlaut_init.rb +113 -0
- data/test/dummy/db/schema.rb +124 -0
- data/test/dummy/log/development.log +12981 -0
- data/test/dummy/log/production.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/tmp/cache/assets/C5F/340/sprockets%2F99692920160b7a279b86a80415b79db7 +0 -0
- data/test/dummy/tmp/cache/assets/C70/4D0/sprockets%2F034ad2036e623081bd352800786dfe80 +0 -0
- data/test/dummy/tmp/cache/assets/C73/920/sprockets%2Fd371318f22900492fd180f17c5e2a504 +9268 -0
- data/test/dummy/tmp/cache/assets/C80/980/sprockets%2Fc94807409c1523d43e18d25f35d93c41 +0 -0
- data/test/dummy/tmp/cache/assets/C8F/780/sprockets%2Fe47e28558116fb5f8038754e60d1961d +11769 -0
- data/test/dummy/tmp/cache/assets/CAA/EB0/sprockets%2F1d179210e8b76f1ea63c802688a015e4 +9271 -0
- data/test/dummy/tmp/cache/assets/CBB/9C0/sprockets%2F706f28923fb754cad04b9107c89986a1 +0 -0
- data/test/dummy/tmp/cache/assets/CBF/B60/sprockets%2F08ca89671549936265dcb673bf02e36f +0 -0
- data/test/dummy/tmp/cache/assets/CC9/9F0/sprockets%2F306166316e2cafd13c15e62b51a2339d +0 -0
- data/test/dummy/tmp/cache/assets/CF6/F20/sprockets%2F5b2ffa1103079dfd555197838f87a99f +0 -0
- data/test/dummy/tmp/cache/assets/CF7/2B0/sprockets%2F25a7c73655bd3598173b39d9f98bcd46 +862 -0
- data/test/dummy/tmp/cache/assets/CFE/080/sprockets%2F37fe9f4255baddbd549a659914929398 +0 -0
- data/test/dummy/tmp/cache/assets/D22/060/sprockets%2F9aec77b768e91a802d284271c58e2f7e +21357 -0
- data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/D33/6D0/sprockets%2F500129c57f1146e556ec3aacd6cd38c1 +0 -0
- data/test/dummy/tmp/cache/assets/D33/FD0/sprockets%2F2ba0b4e6334a77b923e5f770381bb2bf +0 -0
- data/test/dummy/tmp/cache/assets/D42/C20/sprockets%2Fbcf14e437b1582bf93b77670acf8e090 +21353 -0
- data/test/dummy/tmp/cache/assets/D50/A30/sprockets%2F7d8b294ac433db5d056538f8cf7c66b9 +0 -0
- data/test/dummy/tmp/cache/assets/D54/ED0/sprockets%2F71c9fa01091d432b131da3bb73faf3d4 +872 -0
- data/test/dummy/tmp/cache/assets/D65/590/sprockets%2Fc1bb92fc3406a126b7dd302edc96d629 +0 -0
- data/test/dummy/tmp/cache/assets/D71/6B0/sprockets%2Fde558b71b494cf09b1bf055c8dff0353 +0 -0
- data/test/dummy/tmp/cache/assets/D72/610/sprockets%2Fa8c708eeb30ef93de34d755d4f45d023 +859 -0
- data/test/dummy/tmp/cache/assets/D76/AD0/sprockets%2Fe2158cde93188cf5ab6457bc6d6602ec +0 -0
- data/test/dummy/tmp/cache/assets/D7A/E40/sprockets%2F9622ffcc499a57627cd1bb18fe31b8e4 +11772 -0
- data/test/dummy/tmp/cache/assets/D84/210/sprockets%2Fabd0103ccec2b428ac62c94e4c40b384 +0 -0
- data/test/dummy/tmp/cache/assets/D9B/770/sprockets%2F8aacf02eb7dbb0949704b28f27b87e0b +0 -0
- data/test/dummy/tmp/cache/assets/DA6/A80/sprockets%2F92e26d8e58d5bcc8b8f6c25d1b05b9c1 +0 -0
- data/test/dummy/tmp/cache/assets/DE8/790/sprockets%2Fd1333bde2b9aafcc712d11dd09ab35d8 +0 -0
- data/test/dummy/tmp/cache/assets/DF7/F30/sprockets%2F7bc16c4109b17fabe29f8ddbbf732d1c +374 -0
- data/test/dummy/tmp/cache/assets/E03/570/sprockets%2F493bdc0ac14cd4f57fdfe4253f992bde +0 -0
- data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/E0B/4B0/sprockets%2F7988df51a61c81ce6ede4a2d4c8cce4f +377 -0
- data/test/dummy/tmp/cache/assets/E5F/960/sprockets%2Fdc007b6cad5c7ef08e33ec28cfff0ef6 +0 -0
- data/test/fixtures/dispatched_services.yml +5 -0
- data/test/fixtures/permalinks.yml +5 -0
- data/test/fixtures/referent_values.yml +1734 -0
- data/test/fixtures/referents.yml +156 -0
- data/test/fixtures/requests.yml +284 -0
- data/test/fixtures/service_responses.yml +5 -0
- data/test/fixtures/sfx_urls.yml +4 -0
- data/test/performance/browsing_test.rb +9 -0
- data/test/test_helper.rb +10 -0
- data/test/umlaut_test.rb +7 -0
- data/test/unit/aleph_patron_test.rb +39 -0
- data/test/unit/aleph_record_benchmarks.rb +28 -0
- data/test/unit/aleph_record_test.rb +30 -0
- data/test/unit/aws_product_sign_test.rb +93 -0
- data/test/unit/collection_test.rb +76 -0
- data/test/unit/google_book_search_test.rb +101 -0
- data/test/unit/primo_searcher_test.rb +403 -0
- data/test/unit/primo_service_test.rb +939 -0
- data/test/unit/primo_ws_test.rb +131 -0
- data/test/unit/service_response_test.rb +9 -0
- data/test/unit/service_test.rb +33 -0
- metadata +580 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# Elsevier provides publically available and linkable sample cover images
|
2
|
+
# for journals they publish. Thanks Elsevier! This service does nothing
|
3
|
+
# more than take an ISSN and look for a match from Elsevier.
|
4
|
+
class ElsevierCover < Service
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
def service_types_generated
|
8
|
+
return [ServiceTypeValue[:cover_image]]
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
#@base_url = "http://www1.elsevier.com/inca/covers/store/issn/"
|
13
|
+
@base_url = "http://www.extranet.elsevier.com/inca_covers_store/issn/"
|
14
|
+
|
15
|
+
super(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle(request)
|
19
|
+
issn = request.referent.issn
|
20
|
+
|
21
|
+
# We need an ISSN
|
22
|
+
return request.dispatched(self, true) unless issn
|
23
|
+
|
24
|
+
# No hyphens please
|
25
|
+
issn = issn.gsub(/[^0-9X]/, '')
|
26
|
+
|
27
|
+
check_url = @base_url + issn + '.gif'
|
28
|
+
|
29
|
+
# does it exist?
|
30
|
+
if ( url_resolves(check_url) )
|
31
|
+
request.add_service_response(:service => self,
|
32
|
+
:service_type_value => ServiceTypeValue[:cover_image] ,
|
33
|
+
:url => check_url,
|
34
|
+
:size => "medium" )
|
35
|
+
end
|
36
|
+
|
37
|
+
return request.dispatched(self, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def url_resolves(url)
|
41
|
+
uri_obj = URI.parse(url)
|
42
|
+
response = Net::HTTP.start(uri_obj.host, uri_obj.port) {|http|
|
43
|
+
http.head(uri_obj.request_uri)
|
44
|
+
}
|
45
|
+
if (response.kind_of?( Net::HTTPSuccess ))
|
46
|
+
return true
|
47
|
+
elsif ( response.kind_of?(Net::HTTPNotFound))
|
48
|
+
return false
|
49
|
+
else
|
50
|
+
# unexpected condition, raise
|
51
|
+
response.value
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# By default, proxies a URL after checking the EZProxy API to see if
|
2
|
+
# it's proxy-able. But you can set the config param precheck_with_api
|
3
|
+
# to false, and then this will simply automatically proxy all links
|
4
|
+
# from umlaut responses. That is useful if you have your EZProxy
|
5
|
+
# server set to automatically redirect non-proxyable URLs to the original
|
6
|
+
# non-proxied version, the API check may not be neccesary.
|
7
|
+
|
8
|
+
# Required parameters:
|
9
|
+
# proxy_server: hostname of EZProxy instance (no "http://", just hostname)
|
10
|
+
#
|
11
|
+
# optional params:
|
12
|
+
# proxy_password (the ProxyURLPassword parameter in ezproxy.cfg; must be set
|
13
|
+
# to turn on proxy url api feature ).
|
14
|
+
# proxy_url_path: defaults to /proxy_url, the default ezproxy path to call api
|
15
|
+
# exclude_hosts: array of hosts to exclude from proxying _even if_ found in
|
16
|
+
# ezproxy config. Each entry can be a string, in which
|
17
|
+
# case it must match host portion of url exactly. Or it can
|
18
|
+
# be a regexp, which will be tested against entire url.
|
19
|
+
# (supply a string inside // markers. eg '/regexp/' ).
|
20
|
+
#
|
21
|
+
# This service is a link_out_filter service, it must be setup in your
|
22
|
+
# services.yml with "task: link_out_filter ".
|
23
|
+
|
24
|
+
|
25
|
+
class Ezproxy < Service
|
26
|
+
required_config_params :proxy_server
|
27
|
+
|
28
|
+
require 'rexml/document'
|
29
|
+
require 'uri'
|
30
|
+
require 'net/http'
|
31
|
+
require 'cgi'
|
32
|
+
|
33
|
+
def initialize(config)
|
34
|
+
@precheck_with_api = true
|
35
|
+
@display_name = "EZProxy"
|
36
|
+
@proxy_login_path = "/login"
|
37
|
+
|
38
|
+
super(config)
|
39
|
+
|
40
|
+
|
41
|
+
@proxy_url_path ||= "/proxy_url"
|
42
|
+
@proxy_url_path = "/" + @proxy_url_path unless @proxy_url_path[0,1] = '/'
|
43
|
+
|
44
|
+
@exclude ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
# This is meant to be called as task:link_out_filter, it doesn't have an
|
48
|
+
# implementation for handle, it implements link_out_filter() instead.
|
49
|
+
def handle(request)
|
50
|
+
raise "Not implemented."
|
51
|
+
end
|
52
|
+
|
53
|
+
# Hook method called by Umlaut.
|
54
|
+
# Returns a proxied url if it should be proxied, or nil if the url
|
55
|
+
# can not or does not need to be proxied.
|
56
|
+
def link_out_filter(orig_url, service_response, other_args = {})
|
57
|
+
# remove trailing or leading whitespace from url, it makes it
|
58
|
+
# an illegal URL anyway, but maybe we can rescue it? Marc 856's
|
59
|
+
# sometimes have accidental trailing whitespace.
|
60
|
+
orig_url = orig_url.strip
|
61
|
+
|
62
|
+
# bad uri? Forget it.
|
63
|
+
return nil unless valid_url?( orig_url )
|
64
|
+
|
65
|
+
# If it's already proxied, leave it alone.
|
66
|
+
return nil if already_proxied(orig_url)
|
67
|
+
|
68
|
+
return nil if excluded?(orig_url)
|
69
|
+
|
70
|
+
new_url = nil
|
71
|
+
if @precheck_with_api
|
72
|
+
new_url = check_proxy_urls( [orig_url] ).values[0]
|
73
|
+
else
|
74
|
+
new_url = auto_proxy_url(orig_url)
|
75
|
+
end
|
76
|
+
|
77
|
+
return new_url
|
78
|
+
end
|
79
|
+
|
80
|
+
def valid_url?(url)
|
81
|
+
begin
|
82
|
+
raise Exception.new("Empty url!") if url.blank?
|
83
|
+
URI.parse( url )
|
84
|
+
return true
|
85
|
+
rescue Exception => e
|
86
|
+
Rails.logger.error("Bad uri sent to ezproxy service. Can not parse. url: <#{url}>")
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# see @exclude config parameter.
|
92
|
+
def excluded?(url)
|
93
|
+
return false if @exclude.blank?
|
94
|
+
|
95
|
+
@exclude.each do |entry|
|
96
|
+
if ((entry[0,1] == '/') && (entry[entry.length()-1 ,1 ] == '/'))
|
97
|
+
# regexp. Match against entire url.
|
98
|
+
re = Regexp.new( entry )
|
99
|
+
return true if re =~ url
|
100
|
+
elsif (entry.kind_of? Regexp)
|
101
|
+
return true if entry =~ url
|
102
|
+
else
|
103
|
+
# ordinary string. Just match against host.
|
104
|
+
host = URI.parse(url).host
|
105
|
+
return true if host == entry
|
106
|
+
end
|
107
|
+
end
|
108
|
+
# looped through them all, no match?
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
|
112
|
+
# pass in a url, this just mindlessly sends it through your
|
113
|
+
# ezproxy instance.
|
114
|
+
def auto_proxy_url(url)
|
115
|
+
return "http://" + @proxy_server + @proxy_login_path + "?qurl=" + CGI.escape(url)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Pass in an array of URLs. Will determine if they are proxyable by EZProxy.
|
119
|
+
# Returns a hash, where the key is the original URL, and the value is the
|
120
|
+
# proxied url---or nil if could not be proxied.
|
121
|
+
def check_proxy_urls(urls)
|
122
|
+
url_doc = REXML::Document.new
|
123
|
+
doc_root = url_doc.add_element "proxy_url_request", {"password"=>@proxy_password}
|
124
|
+
urls_elem = doc_root.add_element "urls"
|
125
|
+
urls.each { | link |
|
126
|
+
url_elem = urls_elem.add_element "url"
|
127
|
+
url_elem.text = link
|
128
|
+
}
|
129
|
+
begin
|
130
|
+
resp = Net::HTTP.post_form(URI.parse('http://' + @proxy_server+@proxy_url_path), {"xml"=>url_doc.to_s})
|
131
|
+
proxy_doc = REXML::Document.new resp.body
|
132
|
+
rescue Timeout::Error
|
133
|
+
Rails.logger.error "Timed out connecting to EZProxy"
|
134
|
+
return proxy_links
|
135
|
+
rescue Exception => e
|
136
|
+
Rails.logger.error "EZProxy error, NOT proxying URL + #{e}"
|
137
|
+
end
|
138
|
+
|
139
|
+
return_hash = {}
|
140
|
+
REXML::XPath.each(proxy_doc, "/proxy_url_response/proxy_urls/url") { | u |
|
141
|
+
unless (u && u.get_text) # if u is empty... weird, but skip it.
|
142
|
+
Rails.logger.error "EZProxy response seems to be missing some pieces.\n Urls requested: #{urls.join(',')}\n EZProxy api request xml: #{url_doc.to_s}\n EZProxy response: #{proxy_doc.to_s}"
|
143
|
+
end
|
144
|
+
orig_url = u.get_text.value
|
145
|
+
return_hash[orig_url] = nil
|
146
|
+
|
147
|
+
if u.attributes["proxy"] == "true"
|
148
|
+
proxied_url = u.attributes["scheme"]+"://"+u.attributes["hostname"]+":"+u.attributes["port"]+u.attributes["login_path"]
|
149
|
+
if u.attributes["encode"] == "true"
|
150
|
+
proxied_url += CGI::escape(u.get_text.value)
|
151
|
+
else
|
152
|
+
proxied_url += u.get_text.value
|
153
|
+
end
|
154
|
+
|
155
|
+
return_hash[orig_url] = proxied_url
|
156
|
+
|
157
|
+
end
|
158
|
+
}
|
159
|
+
return return_hash
|
160
|
+
end
|
161
|
+
|
162
|
+
# pass in url as a string. Return true if the
|
163
|
+
# url is already pointing to the proxy server
|
164
|
+
# configured.
|
165
|
+
def already_proxied(url)
|
166
|
+
uri_obj = URI.parse(url)
|
167
|
+
|
168
|
+
return uri_obj.host == @proxy_server && uri_obj.path == @proxy_login_path
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
@@ -0,0 +1,442 @@
|
|
1
|
+
# Service that searches Google Book Search to determine viewability.
|
2
|
+
# It searches by ISBN, OCLCNUM and/or LCCN.
|
3
|
+
#
|
4
|
+
# Uses Google Books API, http://code.google.com/apis/books/docs/v1/getting_started.html
|
5
|
+
# http://code.google.com/apis/books/docs/v1/using.html
|
6
|
+
#
|
7
|
+
# If a full view is available it returns a fulltext service response.
|
8
|
+
# If partial view is available, return as "limited experts".
|
9
|
+
# If no view at all, still includes a link in highlighted_links, to pay
|
10
|
+
# lip service to google branding requirements.
|
11
|
+
# Unfortunately there is no way tell which of the noview
|
12
|
+
# books provide search, although some do -- search is advertised if full or
|
13
|
+
# partial view is available.
|
14
|
+
#
|
15
|
+
# If a thumbnail_url is returned in the responses, a cover image is displayed.
|
16
|
+
#
|
17
|
+
# = Google API Key
|
18
|
+
#
|
19
|
+
# Setting an api key in :api_key STRONGLY recommended, or you'll
|
20
|
+
# probably get rate limited (not clear what the limit is with no api
|
21
|
+
# key supplied). You may have to ask for higher rate limit for your api
|
22
|
+
# key than the default 1000/day, which you can do through the google
|
23
|
+
# api console:
|
24
|
+
# https://code.google.com/apis/console
|
25
|
+
#
|
26
|
+
# I requested 50k with this message, and was quickly approved with no questions
|
27
|
+
# "Services for academic library (Johns Hopkins Libraries) web applications to match Google Books availability to items presented by our catalog, OpenURL link resolver, and other software. "
|
28
|
+
#
|
29
|
+
# Recommend setting your 'per user limit' to something crazy high, as well
|
30
|
+
# as requesting more quota.
|
31
|
+
class GoogleBookSearch < Service
|
32
|
+
require 'multi_json'
|
33
|
+
|
34
|
+
|
35
|
+
# Identifiers used in API response to indicate viewability level
|
36
|
+
ViewFullValue = 'ALL_PAGES'
|
37
|
+
ViewPartialValue = 'PARTIAL'
|
38
|
+
# None might also be 'snippet', but Google doesn't want to distinguish
|
39
|
+
ViewNoneValue = 'NO_PAGES'
|
40
|
+
ViewUnknownValue = 'UNKNOWN'
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
include MetadataHelper
|
45
|
+
include UmlautHttp
|
46
|
+
|
47
|
+
# required params
|
48
|
+
|
49
|
+
# attr_reader is important for tests
|
50
|
+
attr_reader :url, :display_name, :num_full_views
|
51
|
+
|
52
|
+
def service_types_generated
|
53
|
+
types= [
|
54
|
+
ServiceTypeValue[:fulltext],
|
55
|
+
ServiceTypeValue[:cover_image],
|
56
|
+
ServiceTypeValue[:highlighted_link],
|
57
|
+
ServiceTypeValue[:search_inside],
|
58
|
+
ServiceTypeValue[:excerpts]]
|
59
|
+
types.push(ServiceTypeValue[:referent_enhance]) if @referent_enhance
|
60
|
+
return types
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(config)
|
64
|
+
@url = 'https://www.googleapis.com/books/v1/volumes?q='
|
65
|
+
|
66
|
+
@display_name = 'Google Books'
|
67
|
+
# number of full views to show
|
68
|
+
@num_full_views = 1
|
69
|
+
# default on, to enhance our metadata with stuff from google
|
70
|
+
@referent_enhance = true
|
71
|
+
# google api key strongly recommended, otherwise you'll
|
72
|
+
# probably get rate limited.
|
73
|
+
@api_key = nil
|
74
|
+
|
75
|
+
@credits = {
|
76
|
+
"Google Books" => "http://books.google.com/"
|
77
|
+
}
|
78
|
+
|
79
|
+
super(config)
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle(request)
|
83
|
+
|
84
|
+
bibkeys = get_bibkeys(request.referent)
|
85
|
+
return request.dispatched(self, true) if bibkeys.nil?
|
86
|
+
data = do_query(bibkeys, request)
|
87
|
+
|
88
|
+
|
89
|
+
if data.blank? || data["error"]
|
90
|
+
# fail fatal
|
91
|
+
return request.dispatched(self, false)
|
92
|
+
end
|
93
|
+
|
94
|
+
# 0 hits, return.
|
95
|
+
return request.dispatched(self, true) if data["totalItems"] == 0
|
96
|
+
|
97
|
+
enhance_referent(request, data) if @referent_enhance
|
98
|
+
|
99
|
+
#return full views first
|
100
|
+
full_views_shown = create_fulltext_service_response(request, data)
|
101
|
+
|
102
|
+
# Add search_inside link if appropriate
|
103
|
+
add_search_inside(request, data)
|
104
|
+
|
105
|
+
# only if no full view is shown, add links for partial view or noview
|
106
|
+
unless full_views_shown
|
107
|
+
do_web_links(request, data)
|
108
|
+
end
|
109
|
+
|
110
|
+
thumbnail_url = find_thumbnail_url(data)
|
111
|
+
if thumbnail_url
|
112
|
+
add_cover_image(request, thumbnail_url)
|
113
|
+
end
|
114
|
+
|
115
|
+
return request.dispatched(self, true)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Take the FIRST hit from google, and use it's values to enhance
|
119
|
+
# our metadata. Will NOT overwrite existing data.
|
120
|
+
def enhance_referent(request, data)
|
121
|
+
|
122
|
+
entry = data["items"].first
|
123
|
+
|
124
|
+
|
125
|
+
if (volumeInfo = entry["volumeInfo"])
|
126
|
+
|
127
|
+
title = volumeInfo["title"]
|
128
|
+
title += ": #{volumeInfo["subtitle"]}" if (title && volumeInfo["subtitle"])
|
129
|
+
|
130
|
+
element_enhance(request, "title", title)
|
131
|
+
element_enhance(request, "au", volumeInfo["authors"].first) if volumeInfo["authors"]
|
132
|
+
element_enhance(request, "pub", volumeInfo["publisher"])
|
133
|
+
|
134
|
+
element_enhance(request, "tpages", volumeInfo["pageCount"])
|
135
|
+
|
136
|
+
if (date = volumeInfo["publishedDate"] && date =~ /^(\d\d\d\d)/)
|
137
|
+
element_enhance(request, "date", $1)
|
138
|
+
end
|
139
|
+
|
140
|
+
# LCCN is only rarely included, but is sometimes, eg:
|
141
|
+
# "industryIdentifiers"=>[{"type"=>"OTHER", "identifier"=>"LCCN:72627172"}],
|
142
|
+
# Also "LCCN:76630875"
|
143
|
+
#
|
144
|
+
# And sometimes OCLC number like:
|
145
|
+
# "industryIdentifiers"=>[{"type"=>"OTHER", "identifier"=>"OCLC:12345678"}],
|
146
|
+
#
|
147
|
+
(volumeInfo["industryIdentifiers"] || []).each do |hash|
|
148
|
+
|
149
|
+
if hash["type"] == "ISBN_13"
|
150
|
+
element_enhance(request, "isbn", hash["identifier"])
|
151
|
+
|
152
|
+
elsif hash["type"] == "OTHER" && hash["identifier"].starts_with?("LCCN:")
|
153
|
+
lccn = normalize_lccn( hash["identifier"].slice(5, hash["identifier"].length) )
|
154
|
+
request.referent.add_identifier("info:lccn/#{lccn}")
|
155
|
+
|
156
|
+
elsif hash["type"] == "OTHER" && hash["identifier"].starts_with?("OCLC:")
|
157
|
+
oclcnum = normalize_lccn( hash["identifier"].slice(5, hash["identifier"].length) )
|
158
|
+
request.referent.add_identifier("info:oclcnum/#{oclcnum}")
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Will not over-write existing referent values.
|
166
|
+
def element_enhance(request, rft_key, value)
|
167
|
+
if (value)
|
168
|
+
request.referent.enhance_referent(rft_key, value.to_s, true, false, :overwrite => false)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# returns nil or escaped string of bibkeys
|
174
|
+
# to increase the chances of good hit, we send all available bibkeys
|
175
|
+
# and later dedupe by id.
|
176
|
+
# FIXME Assumes we only have one of each kind of identifier.
|
177
|
+
def get_bibkeys(rft)
|
178
|
+
isbn = get_identifier(:urn, "isbn", rft)
|
179
|
+
oclcnum = get_identifier(:info, "oclcnum", rft)
|
180
|
+
lccn = get_lccn(rft)
|
181
|
+
|
182
|
+
# Google doesn't officially support oclc/lccn search, but does
|
183
|
+
# index as token with prefix smashed up right with identifier
|
184
|
+
# eg http://books.google.com/books/feeds/volumes?q=OCLC32012617
|
185
|
+
#
|
186
|
+
# Except turns out doing it as a phrase search is important! Or
|
187
|
+
# google's normalization/tokenization does odd things.
|
188
|
+
keys = []
|
189
|
+
keys << ('isbn:' + isbn) if isbn
|
190
|
+
keys << ('"' + "OCLC" + oclcnum + '"') if oclcnum
|
191
|
+
# Only use LCCN if we've got nothing else, it returns many
|
192
|
+
# false positives.
|
193
|
+
keys << ('"' + 'LCCN' + lccn + '"') if lccn && keys.length == 0
|
194
|
+
|
195
|
+
return nil if keys.empty?
|
196
|
+
keys = CGI.escape( keys.join(' OR ') )
|
197
|
+
return keys
|
198
|
+
end
|
199
|
+
|
200
|
+
def do_query(bibkeys, request)
|
201
|
+
headers = build_headers(request)
|
202
|
+
link = @url + bibkeys
|
203
|
+
if @api_key
|
204
|
+
link += "&key=#{@api_key}"
|
205
|
+
end
|
206
|
+
|
207
|
+
# Add on limit to only request books, not magazines.
|
208
|
+
link += "&printType=books"
|
209
|
+
|
210
|
+
Rails.logger.debug("GoogleBookSearch requesting: #{link}")
|
211
|
+
response = http_fetch(link, :headers => headers, :raise_on_http_error_code => false)
|
212
|
+
data = MultiJson.decode(response.body)
|
213
|
+
|
214
|
+
# If Google gives us an error cause it says it can't geo-locate,
|
215
|
+
# remove the IP, log warning, and try again.
|
216
|
+
|
217
|
+
if (data["error"] && data["error"]["errors"] &&
|
218
|
+
data["error"]["errors"].find {|h| h["reason"] == "unknownLocation"} )
|
219
|
+
Rails.logger.warn("GoogleBookSearch: geo-locate error, retrying without X-Forwarded-For: '#{link}' headers: #{headers.inspect} #{response.inspect}\n #{data.inspect}")
|
220
|
+
|
221
|
+
response = http_fetch(link, :raise_on_http_error_code => false)
|
222
|
+
data = MultiJson.decode(response.body)
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
if (! response.kind_of?(Net::HTTPSuccess)) || data["error"]
|
228
|
+
Rails.logger.error("GoogleBookSearch error: '#{link}' headers: #{headers.inspect} #{response.inspect}\n #{data.inspect}")
|
229
|
+
end
|
230
|
+
|
231
|
+
return data
|
232
|
+
end
|
233
|
+
|
234
|
+
# We don't need to fake a proxy request anymore, but we still
|
235
|
+
# include X-Forwarded-For so google can return location-appropriate
|
236
|
+
# availability. If there's an existing X-Forwarded-For, we respect
|
237
|
+
# it and add on to it.
|
238
|
+
def build_headers(request)
|
239
|
+
original_forwarded_for = nil
|
240
|
+
if (request.http_env && request.http_env['HTTP_X_FORWARDED_FOR'])
|
241
|
+
original_forwarded_for = request.http_env['HTTP_X_FORWARDED_FOR']
|
242
|
+
end
|
243
|
+
|
244
|
+
# we used to prepare a comma seperated list in x-forwarded-for if
|
245
|
+
# we had multiple requests, as per the x-forwarded-for spec, but I
|
246
|
+
# think Google doesn't like it.
|
247
|
+
|
248
|
+
ip_address = (original_forwarded_for ?
|
249
|
+
original_forwarded_for :
|
250
|
+
request.client_ip_addr.to_s)
|
251
|
+
|
252
|
+
return {} if ip_address.blank?
|
253
|
+
|
254
|
+
# If we've got a comma-seperated list from an X-Forwarded-For, we
|
255
|
+
# can't send it on to google, google won't accept that, just take
|
256
|
+
# the first one in the list, which is actually the ultimate client
|
257
|
+
# IP. split returns the whole string if seperator isn't found, convenient.
|
258
|
+
ip_address = ip_address.split(",").first
|
259
|
+
|
260
|
+
# If all we have is an internal/private IP from the internal network,
|
261
|
+
# do NOT send that to Google, or Google will give you a 503 error
|
262
|
+
# and refuse to process your request, as of 7 sep 2011. sigh.
|
263
|
+
# Also if it doesn't look like an IP at all, forget it, don't send it.
|
264
|
+
if ((! ip_address =~ /^\d+\.\d+\.\d+\/\d$/) ||
|
265
|
+
ip_address.start_with?("10.") ||
|
266
|
+
ip_address.start_with?("172.16") ||
|
267
|
+
ip_address.start_with?("192.168"))
|
268
|
+
return {}
|
269
|
+
else
|
270
|
+
return {'X-Forwarded-For' => ip_address }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def find_entries(gbs_response, viewabilities)
|
275
|
+
unless (viewabilities.kind_of?(Array))
|
276
|
+
viewabilities = [viewabilities]
|
277
|
+
end
|
278
|
+
|
279
|
+
entries = gbs_response["items"].find_all do |entry|
|
280
|
+
viewability = entry["accessInfo"]["viewability"]
|
281
|
+
(viewability && viewabilities.include?(viewability))
|
282
|
+
end
|
283
|
+
|
284
|
+
return entries
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
# We only create a fulltext service response if we have a full view.
|
289
|
+
# We create only as many full views as are specified in config.
|
290
|
+
def create_fulltext_service_response(request, data)
|
291
|
+
display_name = @display_name
|
292
|
+
|
293
|
+
full_views = find_entries(data, ViewFullValue)
|
294
|
+
return nil if full_views.empty?
|
295
|
+
|
296
|
+
count = 0
|
297
|
+
full_views.each do |fv|
|
298
|
+
|
299
|
+
uri = fv["volumeInfo"]["previewLink"]
|
300
|
+
|
301
|
+
request.add_service_response(
|
302
|
+
:service => self,
|
303
|
+
:display_text => display_name,
|
304
|
+
:url => remove_query_context(uri),
|
305
|
+
:service_type_value => :fulltext
|
306
|
+
)
|
307
|
+
count += 1
|
308
|
+
break if count == @num_full_views
|
309
|
+
end
|
310
|
+
return true
|
311
|
+
end
|
312
|
+
|
313
|
+
def add_search_inside(request, data)
|
314
|
+
# Just take the first one we find, if multiple
|
315
|
+
searchable_view = find_entries(data, [ViewFullValue, ViewPartialValue])[0]
|
316
|
+
|
317
|
+
if ( searchable_view )
|
318
|
+
url = searchable_view["volumeInfo"]["infoLink"]
|
319
|
+
|
320
|
+
request.add_service_response(
|
321
|
+
:service => self,
|
322
|
+
:display_text=>@display_name,
|
323
|
+
:url=> remove_query_context(url),
|
324
|
+
:service_type_value => :search_inside
|
325
|
+
)
|
326
|
+
end
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
# create highlighted_link service response for partial and noview
|
331
|
+
# Only show one web link. prefer a partial view over a noview.
|
332
|
+
# Some noviews have a snippet/search, but we have no way to tell.
|
333
|
+
def do_web_links(request, data)
|
334
|
+
|
335
|
+
# some noview items will have a snippet view, but we have no way to tell
|
336
|
+
info_views = find_entries(data, ViewPartialValue)
|
337
|
+
viewability = ViewPartialValue
|
338
|
+
|
339
|
+
if info_views.blank?
|
340
|
+
info_views = find_entries(data, ViewNoneValue)
|
341
|
+
viewability = ViewNoneValue
|
342
|
+
end
|
343
|
+
|
344
|
+
# Shouldn't ever get to this point, but just in case
|
345
|
+
return nil if info_views.blank?
|
346
|
+
|
347
|
+
url = ''
|
348
|
+
iv = info_views.first
|
349
|
+
type = nil
|
350
|
+
if (viewability == ViewPartialValue &&
|
351
|
+
url = iv["volumeInfo"]["previewLink"])
|
352
|
+
display_text = @display_name
|
353
|
+
type = ServiceTypeValue[:excerpts]
|
354
|
+
else
|
355
|
+
url = url = iv["volumeInfo"]["infoLink"]
|
356
|
+
display_text = "Book Information"
|
357
|
+
type = ServiceTypeValue[:highlighted_link]
|
358
|
+
end
|
359
|
+
request.add_service_response(
|
360
|
+
:service=>self,
|
361
|
+
:url=> remove_query_context(url),
|
362
|
+
:display_text=>display_text,
|
363
|
+
:service_type_value => type
|
364
|
+
)
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
|
369
|
+
|
370
|
+
# Not all responses have a thumbnail_url. We look for them and return the 1st.
|
371
|
+
def find_thumbnail_url(data)
|
372
|
+
entries = data["items"].collect do |entry|
|
373
|
+
entry["volumeInfo"]["imageLinks"]["thumbnail"] if entry["volumeInfo"] && entry["volumeInfo"]["imageLinks"]
|
374
|
+
end
|
375
|
+
|
376
|
+
# removenill values
|
377
|
+
entries.compact!
|
378
|
+
|
379
|
+
# pick the first of the available thumbnails, or nil
|
380
|
+
return entries[0]
|
381
|
+
end
|
382
|
+
|
383
|
+
|
384
|
+
def add_cover_image(request, url)
|
385
|
+
zoom_url = url.clone
|
386
|
+
|
387
|
+
# if we're sent to a page other than the frontcover then strip out the
|
388
|
+
# page number and insert front cover
|
389
|
+
zoom_url.sub!(/&pg=.*?&/, '&printsec=frontcover&')
|
390
|
+
|
391
|
+
# hack out the 'curl' if we can
|
392
|
+
zoom_url.sub!('&edge=curl', '')
|
393
|
+
|
394
|
+
request.add_service_response(
|
395
|
+
:service=>self,
|
396
|
+
:display_text => 'Cover Image',
|
397
|
+
:url => zoom_url,
|
398
|
+
:size => "medium",
|
399
|
+
:service_type_value => :cover_image
|
400
|
+
)
|
401
|
+
end
|
402
|
+
|
403
|
+
# Google gives us URL to the book that contains a 'dq' param
|
404
|
+
# with the original query, which for us is an ISSN/LCCN/OCLCnum query,
|
405
|
+
# which we don't actually want to leave in there.
|
406
|
+
def remove_query_context(url)
|
407
|
+
url.sub(/&dq=[^&]+/, '')
|
408
|
+
end
|
409
|
+
|
410
|
+
# Catch url_for call for search_inside, because we're going to redirect
|
411
|
+
def response_url(service_response, submitted_params)
|
412
|
+
if ( ! (service_response.service_type_value.name == "search_inside" ))
|
413
|
+
return super(service_response, submitted_params)
|
414
|
+
else
|
415
|
+
# search inside!
|
416
|
+
base = service_response[:url]
|
417
|
+
query = CGI.escape(submitted_params["query"] || "")
|
418
|
+
# attempting to reverse engineer a bit to get 'snippet'
|
419
|
+
# style results instead of 'onepage' style results.
|
420
|
+
# snippet seem more user friendly, and are what google's own
|
421
|
+
# interface seems to give you by default. but 'onepage' is the
|
422
|
+
# default from our deep link, but if we copy the JS hash data,
|
423
|
+
# it looks like we can get Google to 'snippet'.
|
424
|
+
url = base + "&q=#{query}#v=snippet&q=#{query}&f=false"
|
425
|
+
return url
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
end
|
430
|
+
|
431
|
+
# Important to quote search, see: "OCLC1246014"
|
432
|
+
|
433
|
+
# Test WorldCat links
|
434
|
+
# FIXME: This produces two 'noview' links because the ids don't match.
|
435
|
+
# This might be as good as we can do though, unless we want to only ever show
|
436
|
+
# one 'noview' link. Notice that the metadata does differ between the two.
|
437
|
+
# http://localhost:3000/resolve?url_ver=Z39.88-2004&rfr_id=info%3Asid%2Fworldcat.org%3Aworldcat&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&req_dat=%3Csessionid%3E&rft_id=info%3Aoclcnum%2F34576818&rft_id=urn%3AISBN%3A9780195101386&rft_id=urn%3AISSN%3A&rft.aulast=Twain&rft.aufirst=Mark&rft.auinitm=&rft.btitle=The+prince+and+the+pauper&rft.atitle=&rft.date=1996&rft.tpages=&rft.isbn=9780195101386&rft.aucorp=&rft.place=New+York&rft.pub=Oxford+University+Press&rft.edition=&rft.series=&rft.genre=book&url_ver=Z39.88-2004
|
438
|
+
#
|
439
|
+
# Snippet view returns noview through the API
|
440
|
+
# http://localhost:3000/resolve?rft.isbn=0155374656
|
441
|
+
#
|
442
|
+
# full view example, LCCN 07020699 ; OCLC: 1246014
|