oai 0.0.1 → 1.3.0
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.
- checksums.yaml +7 -0
- data/README.md +128 -0
- data/Rakefile +64 -0
- data/bin/oai +66 -0
- data/examples/models/file_model.rb +63 -0
- data/examples/providers/dublin_core.rb +474 -0
- data/lib/oai/{get_record.rb → client/get_record.rb} +5 -1
- data/lib/oai/client/header.rb +18 -0
- data/lib/oai/{identify.rb → client/identify.rb} +2 -0
- data/lib/oai/{list_identifiers.rb → client/list_identifiers.rb} +1 -0
- data/lib/oai/{list_records.rb → client/list_records.rb} +6 -5
- data/lib/oai/{list_sets.rb → client/list_sets.rb} +7 -6
- data/lib/oai/client/record.rb +27 -0
- data/lib/oai/client/response.rb +53 -0
- data/lib/oai/client/resumable.rb +42 -0
- data/lib/oai/client.rb +313 -108
- data/lib/oai/constants.rb +34 -0
- data/lib/oai/exception.rb +80 -1
- data/lib/oai/harvester/config.rb +41 -0
- data/lib/oai/harvester/harvest.rb +162 -0
- data/lib/oai/harvester/logging.rb +68 -0
- data/lib/oai/harvester/mailer.rb +17 -0
- data/lib/oai/harvester/shell.rb +337 -0
- data/lib/oai/harvester.rb +43 -0
- data/lib/oai/provider/metadata_format/oai_dc.rb +28 -0
- data/lib/oai/provider/metadata_format.rb +119 -0
- data/lib/oai/provider/model/activerecord_caching_wrapper.rb +129 -0
- data/lib/oai/provider/model/activerecord_wrapper.rb +217 -0
- data/lib/oai/provider/model.rb +80 -0
- data/lib/oai/provider/partial_result.rb +18 -0
- data/lib/oai/provider/response/error.rb +16 -0
- data/lib/oai/provider/response/get_record.rb +27 -0
- data/lib/oai/provider/response/identify.rb +37 -0
- data/lib/oai/provider/response/list_identifiers.rb +30 -0
- data/lib/oai/provider/response/list_metadata_formats.rb +39 -0
- data/lib/oai/provider/response/list_records.rb +47 -0
- data/lib/oai/provider/response/list_sets.rb +23 -0
- data/lib/oai/provider/response/record_response.rb +103 -0
- data/lib/oai/provider/response.rb +133 -0
- data/lib/oai/provider/resumption_token.rb +155 -0
- data/lib/oai/provider.rb +487 -0
- data/lib/oai/set.rb +14 -5
- data/lib/oai/xpath.rb +59 -5
- data/lib/oai.rb +7 -13
- data/lib/test.rb +25 -0
- data/test/activerecord_provider/config/connection.rb +30 -0
- data/test/activerecord_provider/database/0001_oaipmh_tables.rb +70 -0
- data/test/activerecord_provider/fixtures/dc.yml +1501 -0
- data/test/activerecord_provider/helpers/providers.rb +68 -0
- data/test/activerecord_provider/helpers/set_provider.rb +24 -0
- data/test/activerecord_provider/helpers/transactional_test_case.rb +35 -0
- data/test/activerecord_provider/models/dc_field.rb +15 -0
- data/test/activerecord_provider/models/dc_lang.rb +3 -0
- data/test/activerecord_provider/models/dc_set.rb +7 -0
- data/test/activerecord_provider/models/exclusive_set_dc_field.rb +17 -0
- data/test/activerecord_provider/models/oai_token.rb +3 -0
- data/test/activerecord_provider/tc_activerecord_wrapper.rb +63 -0
- data/test/activerecord_provider/tc_ar_provider.rb +199 -0
- data/test/activerecord_provider/tc_ar_sets_provider.rb +159 -0
- data/test/activerecord_provider/tc_caching_paging_provider.rb +50 -0
- data/test/activerecord_provider/tc_simple_paging_provider.rb +71 -0
- data/test/activerecord_provider/test_helper_ar_provider.rb +7 -0
- data/test/client/helpers/provider.rb +65 -0
- data/test/client/helpers/test_wrapper.rb +6 -0
- data/test/client/tc_exception.rb +36 -0
- data/test/client/tc_get_record.rb +39 -0
- data/test/client/tc_http_client.rb +129 -0
- data/test/client/tc_identify.rb +13 -0
- data/test/client/tc_libxml.rb +61 -0
- data/test/{tc_list_identifiers.rb → client/tc_list_identifiers.rb} +28 -9
- data/test/{tc_list_metadata_formats.rb → client/tc_list_metadata_formats.rb} +4 -1
- data/test/client/tc_list_records.rb +26 -0
- data/test/client/tc_list_sets.rb +34 -0
- data/test/client/tc_low_resolution_dates.rb +14 -0
- data/test/client/tc_utf8_escaping.rb +19 -0
- data/test/client/tc_xpath.rb +26 -0
- data/test/client/test_helper_client.rb +5 -0
- data/test/harvester/tc_harvest.rb +42 -0
- data/test/harvester/test_helper_harvester.rb +6 -0
- data/test/provider/models.rb +246 -0
- data/test/provider/tc_exceptions.rb +81 -0
- data/test/provider/tc_functional_tokens.rb +53 -0
- data/test/provider/tc_instance_provider.rb +41 -0
- data/test/provider/tc_provider.rb +136 -0
- data/test/provider/tc_resumption_tokens.rb +52 -0
- data/test/provider/tc_simple_provider.rb +138 -0
- data/test/provider/test_helper_provider.rb +62 -0
- data/test/test.xml +22 -0
- metadata +238 -56
- data/lib/oai/header.rb +0 -12
- data/lib/oai/record.rb +0 -11
- data/lib/oai/response.rb +0 -20
- data/test/tc_get_record.rb +0 -27
- data/test/tc_identify.rb +0 -8
- data/test/tc_list_records.rb +0 -9
- data/test/tc_list_sets.rb +0 -17
- data/test.rb +0 -12
- /data/lib/oai/{list_metadata_formats.rb → client/list_metadata_formats.rb} +0 -0
- /data/lib/oai/{metadata_format.rb → client/metadata_format.rb} +0 -0
data/lib/oai/client.rb
CHANGED
|
@@ -1,163 +1,368 @@
|
|
|
1
|
+
# encoding: us-ascii
|
|
2
|
+
|
|
3
|
+
# External dependencies
|
|
1
4
|
require 'uri'
|
|
2
|
-
require '
|
|
3
|
-
require 'rexml/document'
|
|
5
|
+
require 'faraday'
|
|
4
6
|
require 'cgi'
|
|
5
7
|
|
|
8
|
+
if not defined?(OAI::Const::VERBS)
|
|
9
|
+
# Shared stuff
|
|
10
|
+
require 'oai/exception'
|
|
11
|
+
require 'oai/constants'
|
|
12
|
+
require 'oai/xpath'
|
|
13
|
+
require 'oai/set'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Localize requires so user can select a subset of functionality
|
|
17
|
+
require 'oai/client/metadata_format'
|
|
18
|
+
require 'oai/client/response'
|
|
19
|
+
require 'oai/client/header'
|
|
20
|
+
require 'oai/client/record'
|
|
21
|
+
require 'oai/client/identify'
|
|
22
|
+
require 'oai/client/get_record'
|
|
23
|
+
require 'oai/client/resumable'
|
|
24
|
+
require 'oai/client/list_identifiers'
|
|
25
|
+
require 'oai/client/list_metadata_formats'
|
|
26
|
+
require 'oai/client/list_records'
|
|
27
|
+
require 'oai/client/list_sets'
|
|
28
|
+
|
|
6
29
|
module OAI
|
|
7
30
|
|
|
8
|
-
# A OAI::Client provides a client api for issuing OAI-PMH verbs against
|
|
9
|
-
# a OAI-PMH server. The 6 OAI-PMH verbs translate directly to methods you
|
|
10
|
-
# can call on a OAI::Client object. Verb arguments are passed as a hash:
|
|
31
|
+
# A `OAI::Client` provides a client api for issuing OAI-PMH verbs against
|
|
32
|
+
# a OAI-PMH server. The 6 OAI-PMH verbs translate directly to methods you
|
|
33
|
+
# can call on a `OAI::Client` object. Verb arguments are passed as a hash:
|
|
11
34
|
#
|
|
12
|
-
#
|
|
13
|
-
# client
|
|
35
|
+
# ```ruby
|
|
36
|
+
# client = OAI::Client.new 'http://www.pubmedcentral.gov/oai/oai.cgi'
|
|
37
|
+
# record = client.get_record :identifier => 'oai:pubmedcentral.gov:13901'
|
|
38
|
+
# for identifier in client.list_identifiers
|
|
39
|
+
# puts identifier
|
|
40
|
+
# end
|
|
41
|
+
# ```
|
|
14
42
|
#
|
|
15
|
-
# It is worth noting that the
|
|
16
|
-
# underscores in them rather than studly caps. So above list_identifiers
|
|
17
|
-
# and metadata_prefix are used instead of the listIdentifiers and
|
|
18
|
-
# metadataPrefix used in the OAI-PMH specification.
|
|
43
|
+
# It is worth noting that the API uses methods and parameter names with
|
|
44
|
+
# underscores in them rather than studly caps. So above `list_identifiers`
|
|
45
|
+
# and `metadata_prefix` are used instead of the `listIdentifiers` and
|
|
46
|
+
# `metadataPrefix` used in the OAI-PMH specification.
|
|
19
47
|
#
|
|
20
48
|
# Also, the from and until arguments which specify dates should be passed
|
|
21
|
-
# in as Date or DateTime objects depending on the granularity supported
|
|
49
|
+
# in as `Date` or `DateTime` objects depending on the granularity supported
|
|
22
50
|
# by the server.
|
|
23
51
|
#
|
|
24
52
|
# For detailed information on the arguments that can be used please consult
|
|
25
|
-
# the OAI-PMH docs at
|
|
26
|
-
#
|
|
27
|
-
# http://www.openarchives.org/OAI/openarchivesprotocol.html
|
|
28
|
-
|
|
29
|
-
class Client
|
|
53
|
+
# the OAI-PMH docs at
|
|
54
|
+
# <http://www.openarchives.org/OAI/openarchivesprotocol.html>.
|
|
30
55
|
|
|
31
|
-
|
|
56
|
+
class Client
|
|
57
|
+
UNESCAPED_AMPERSAND = /&(?!(?:amp|lt|gt|quot|apos|\#\d+);)/
|
|
58
|
+
# The constructor which must be passed a valid base url for an oai
|
|
32
59
|
# service:
|
|
33
60
|
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
# client = OAI::Client.new 'http://www.pubmedcentral.gov/oai/oai.cgi'
|
|
62
|
+
#
|
|
63
|
+
# If you want to see debugging messages on `STDERR` use:
|
|
64
|
+
#
|
|
65
|
+
# client = OAI::Client.new 'http://example.com', :debug => true
|
|
66
|
+
#
|
|
67
|
+
# By default OAI verbs called on the client will return `REXML::Element`
|
|
68
|
+
# objects for metadata records, however if you wish you can use the
|
|
69
|
+
# `:parser` option to indicate you want to use `libxml` instead, and get
|
|
70
|
+
# back `XML::Node` objects
|
|
71
|
+
#
|
|
72
|
+
# client = OAI::Client.new 'http://example.com', :parser => 'libxml'
|
|
73
|
+
#
|
|
74
|
+
# You can configure the Faraday HTTP client by providing an alternate
|
|
75
|
+
# Faraday instance:
|
|
76
|
+
#
|
|
77
|
+
# ```ruby
|
|
78
|
+
# client = OAI::Client.new 'http://example.com', :http => Faraday.new {|c|}
|
|
79
|
+
# ```
|
|
80
|
+
#
|
|
81
|
+
# ### HIGH PERFORMANCE
|
|
82
|
+
#
|
|
83
|
+
# If you want to supercharge this api install `libxml-ruby >= 0.3.8` and
|
|
84
|
+
# use the `:parser` option when you construct your `OAI::Client`.
|
|
85
|
+
#
|
|
86
|
+
def initialize(base_url, options={})
|
|
37
87
|
@base = URI.parse base_url
|
|
88
|
+
@debug = options.fetch(:debug, false)
|
|
89
|
+
@parser = options.fetch(:parser, 'rexml')
|
|
90
|
+
@headers = options.fetch(:headers, {})
|
|
91
|
+
|
|
92
|
+
@http_client = options.fetch(:http) do
|
|
93
|
+
Faraday.new(:url => @base.clone) do |builder|
|
|
94
|
+
follow_redirects = options.fetch(:redirects, true)
|
|
95
|
+
follow_redirects = 5 if follow_redirects == true
|
|
96
|
+
|
|
97
|
+
if follow_redirects
|
|
98
|
+
require 'faraday/follow_redirects'
|
|
99
|
+
builder.use Faraday::FollowRedirects::Middleware
|
|
100
|
+
builder.response :follow_redirects, :limit => follow_redirects.to_i
|
|
101
|
+
end
|
|
102
|
+
builder.adapter :net_http
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# load appropriate parser
|
|
107
|
+
case @parser
|
|
108
|
+
when 'libxml'
|
|
109
|
+
begin
|
|
110
|
+
require 'rubygems'
|
|
111
|
+
require 'xml/libxml'
|
|
112
|
+
rescue
|
|
113
|
+
raise OAI::Exception.new("xml/libxml not available")
|
|
114
|
+
end
|
|
115
|
+
when 'rexml'
|
|
116
|
+
require 'rexml/document'
|
|
117
|
+
require 'rexml/xpath'
|
|
118
|
+
else
|
|
119
|
+
raise OAI::Exception.new("unknown parser: #{@parser}")
|
|
120
|
+
end
|
|
38
121
|
end
|
|
39
122
|
|
|
40
|
-
# Equivalent to a Identify request.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
123
|
+
# Equivalent to a `Identify` request.
|
|
124
|
+
# You'll get back a `OAI::IdentifyResponse`
|
|
125
|
+
# object which is essentially just a wrapper around a `REXML::Document`
|
|
126
|
+
# for the response. If you created your client using the `libxml`
|
|
127
|
+
# parser then you will get an `XML::Node` object instead.
|
|
44
128
|
def identify
|
|
45
|
-
|
|
129
|
+
OAI::IdentifyResponse.new(do_request('Identify'))
|
|
46
130
|
end
|
|
47
131
|
|
|
48
|
-
# Equivalent to a ListMetadataFormats request.
|
|
49
|
-
# object is returned to you.
|
|
50
|
-
|
|
132
|
+
# Equivalent to a `ListMetadataFormats` request.
|
|
133
|
+
# A `ListMetadataFormatsResponse` object is returned to you.
|
|
134
|
+
|
|
51
135
|
def list_metadata_formats(opts={})
|
|
52
|
-
|
|
53
|
-
verify_verb_arguments opts, [:verb, :identifier]
|
|
54
|
-
return ListMetadataFormatsResponse.new(do_request(opts))
|
|
136
|
+
OAI::ListMetadataFormatsResponse.new(do_request('ListMetadataFormats', opts))
|
|
55
137
|
end
|
|
56
138
|
|
|
57
|
-
# Equivalent to a ListIdentifiers request. Pass in
|
|
58
|
-
# as Date or DateTime objects as appropriate
|
|
59
|
-
# supported by the server.
|
|
60
|
-
|
|
139
|
+
# Equivalent to a `ListIdentifiers` request. Pass in `:from`,
|
|
140
|
+
# `:until` arguments as `Date` or `DateTime` objects as appropriate
|
|
141
|
+
# depending on the granularity supported by the server.
|
|
142
|
+
#
|
|
143
|
+
# You can use seamless resumption with this verb, which allows you to
|
|
144
|
+
# mitigate (to some extent) the lack of a `Count` verb:
|
|
145
|
+
#
|
|
146
|
+
# client.list_identifiers.full.count # Don't try this on PubMed though!
|
|
147
|
+
#
|
|
61
148
|
def list_identifiers(opts={})
|
|
62
|
-
|
|
63
|
-
add_default_metadata_prefix opts
|
|
64
|
-
verify_verb_arguments opts, [:verb, :from, :until, :metadata_prefix, :set, :resumption_token]
|
|
65
|
-
return ListIdentifiersResponse.new(do_request(opts))
|
|
149
|
+
do_resumable(OAI::ListIdentifiersResponse, 'ListIdentifiers', opts)
|
|
66
150
|
end
|
|
67
151
|
|
|
68
|
-
# Equivalent to a GetRecord request. You must supply an identifier
|
|
69
|
-
# argument. You should get back a OAI::GetRecordResponse object
|
|
70
|
-
# which you can extract a OAI::Record object from.
|
|
71
|
-
|
|
152
|
+
# Equivalent to a `GetRecord` request. You must supply an `:identifier`
|
|
153
|
+
# argument. You should get back a `OAI::GetRecordResponse` object
|
|
154
|
+
# which you can extract a `OAI::Record` object from.
|
|
72
155
|
def get_record(opts={})
|
|
73
|
-
|
|
74
|
-
add_default_metadata_prefix opts
|
|
75
|
-
verify_verb_arguments opts, [:verb, :identifier, :metadata_prefix]
|
|
76
|
-
return GetRecordResponse.new(do_request(opts))
|
|
156
|
+
OAI::GetRecordResponse.new(do_request('GetRecord', opts))
|
|
77
157
|
end
|
|
78
158
|
|
|
79
|
-
# Equivalent to the ListRecords request. A ListRecordsResponse
|
|
159
|
+
# Equivalent to the `ListRecords` request. A `ListRecordsResponse`
|
|
80
160
|
# will be returned which you can use to iterate through records
|
|
81
161
|
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
162
|
+
# response = client.list_records
|
|
163
|
+
# response.each do |record|
|
|
164
|
+
# puts record.metadata
|
|
165
|
+
# end
|
|
166
|
+
#
|
|
167
|
+
# Alternately, you can use seamless resumption to avoid handling
|
|
168
|
+
# resumption tokens:
|
|
169
|
+
#
|
|
170
|
+
# client.list_records.full.each do |record|
|
|
171
|
+
# puts record.metadata
|
|
172
|
+
# end
|
|
173
|
+
#
|
|
174
|
+
# ### Memory Use
|
|
175
|
+
# `:full` will avoid storing more than one page of records in
|
|
176
|
+
# memory, but your use it in ways that override that behaviour. Be careful
|
|
177
|
+
# to avoid using `client.list_records.full.entries` unless you really want
|
|
178
|
+
# to hold all the records in the feed in memory!
|
|
86
179
|
def list_records(opts={})
|
|
87
|
-
|
|
88
|
-
add_default_metadata_prefix opts
|
|
89
|
-
verify_verb_arguments opts, [:verb, :from, :until, :set,
|
|
90
|
-
:resumption_token, :metadata_prefix]
|
|
91
|
-
return ListRecordsResponse.new(do_request(opts))
|
|
180
|
+
do_resumable(OAI::ListRecordsResponse, 'ListRecords', opts)
|
|
92
181
|
end
|
|
93
182
|
|
|
94
|
-
# Equivalent to the ListSets request. A ListSetsResponse object
|
|
95
|
-
# will be returned which you can use for iterating through the
|
|
96
|
-
# OAI::Set objects
|
|
183
|
+
# Equivalent to the `ListSets` request. A `ListSetsResponse` object
|
|
184
|
+
# will be returned which you can use for iterating through the
|
|
185
|
+
# `OAI::Set` objects
|
|
186
|
+
#
|
|
187
|
+
# for set in client.list_sets
|
|
188
|
+
# puts set
|
|
189
|
+
# end
|
|
97
190
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
191
|
+
# A large number of sets is not unusual for some OAI-PMH feeds, so
|
|
192
|
+
# using seamless resumption may be preferable:
|
|
193
|
+
#
|
|
194
|
+
# client.list_sets.full.each do |set|
|
|
195
|
+
# puts set
|
|
196
|
+
# end
|
|
102
197
|
def list_sets(opts={})
|
|
103
|
-
|
|
104
|
-
verify_verb_arguments opts, [:verb, :resumptionToken]
|
|
105
|
-
return ListSetsResponse.new(do_request(opts))
|
|
198
|
+
do_resumable(OAI::ListSetsResponse, 'ListSets', opts)
|
|
106
199
|
end
|
|
107
200
|
|
|
108
|
-
|
|
201
|
+
def sanitize_xml(xml)
|
|
202
|
+
xml = strip_invalid_utf_8_chars(xml)
|
|
203
|
+
xml = strip_invalid_xml_chars(xml)
|
|
204
|
+
if @parser == 'libxml'
|
|
205
|
+
# remove default namespace for oai-pmh since libxml
|
|
206
|
+
# isn't able to use our xpaths to get at them
|
|
207
|
+
# if you know a way around thins please let me know
|
|
208
|
+
xml = xml.gsub(
|
|
209
|
+
/xmlns=\"http:\/\/www.openarchives.org\/OAI\/.\..\/\"/, '')
|
|
210
|
+
end
|
|
211
|
+
xml
|
|
212
|
+
end
|
|
109
213
|
|
|
110
|
-
|
|
111
|
-
uri = @base.clone
|
|
214
|
+
private
|
|
112
215
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"#{key}=#{value}"
|
|
216
|
+
def do_request(verb, opts = nil)
|
|
217
|
+
# fire off the request and return appropriate DOM object
|
|
218
|
+
uri = build_uri(verb, opts)
|
|
219
|
+
return load_document(get(uri))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def do_resumable(responseClass, verb, opts)
|
|
223
|
+
responseClass.new(do_request(verb, opts)) do |response|
|
|
224
|
+
responseClass.new \
|
|
225
|
+
do_request(verb, :resumption_token => response.resumption_token)
|
|
124
226
|
end
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build_uri(verb, opts)
|
|
230
|
+
opts = validate_options(verb, opts)
|
|
231
|
+
uri = @base
|
|
232
|
+
uri.query = "verb=" << verb
|
|
233
|
+
opts.each_pair { |k,v| uri.query << '&' << externalize(k) << '=' << encode(v) }
|
|
234
|
+
uri
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def encode(value)
|
|
238
|
+
return CGI.escape(value) unless value.respond_to?(:strftime)
|
|
239
|
+
if value.kind_of?(DateTime)
|
|
240
|
+
Time.parse(value.asctime).utc.xmlschema
|
|
241
|
+
elsif value.kind_of?(Time)
|
|
242
|
+
value.utc.xmlschema
|
|
243
|
+
else # Assume something date like
|
|
244
|
+
value.strftime('%Y-%m-%d')
|
|
133
245
|
end
|
|
134
246
|
end
|
|
135
247
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
248
|
+
def load_document(xml)
|
|
249
|
+
xml = sanitize_xml(xml)
|
|
250
|
+
case @parser
|
|
251
|
+
when 'libxml'
|
|
252
|
+
begin
|
|
253
|
+
parser = XML::Parser.string(xml)
|
|
254
|
+
return parser.parse
|
|
255
|
+
rescue XML::Error => e
|
|
256
|
+
raise OAI::Exception, 'response not well formed XML: '+e, caller
|
|
257
|
+
end
|
|
258
|
+
when 'rexml'
|
|
259
|
+
begin
|
|
260
|
+
return REXML::Document.new(xml)
|
|
261
|
+
rescue REXML::ParseException => e
|
|
262
|
+
raise OAI::Exception, 'response not well formed XML: '+e.message, caller
|
|
263
|
+
end
|
|
142
264
|
end
|
|
143
265
|
end
|
|
144
266
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
267
|
+
# Do the actual HTTP get, following any temporary redirects
|
|
268
|
+
def get(uri)
|
|
269
|
+
response = @http_client.get do |req|
|
|
270
|
+
req.url uri
|
|
271
|
+
req.headers.merge! @headers
|
|
150
272
|
end
|
|
273
|
+
|
|
274
|
+
response.body
|
|
151
275
|
end
|
|
152
276
|
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
277
|
+
def debug(msg)
|
|
278
|
+
$stderr.print("#{msg}\n") if @debug
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Massage the standard OAI options to make them a bit more palatable.
|
|
282
|
+
def validate_options(verb, opts = {})
|
|
283
|
+
raise OAI::VerbException.new unless Const::VERBS.keys.include?(verb)
|
|
284
|
+
|
|
285
|
+
return {} if opts.nil?
|
|
286
|
+
|
|
287
|
+
raise OAI::ArgumentException.new unless opts.respond_to?(:keys)
|
|
288
|
+
|
|
289
|
+
realopts = {}
|
|
290
|
+
# Internalize the hash
|
|
291
|
+
opts.keys.each do |key|
|
|
292
|
+
realopts[key.to_s.gsub(/([A-Z])/, '_\1').downcase.intern] = opts.delete(key)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
return realopts if is_resumption?(realopts)
|
|
296
|
+
|
|
297
|
+
# add in a default metadataPrefix if none exists
|
|
298
|
+
if(Const::VERBS[verb].include?(:metadata_prefix))
|
|
299
|
+
realopts[:metadata_prefix] ||= 'oai_dc'
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Convert date formated strings in dates.
|
|
303
|
+
#realopts[:from] = parse_date(realopts[:from]) if realopts[:from]
|
|
304
|
+
#realopts[:until] = parse_date(realopts[:until]) if realopts[:until]
|
|
305
|
+
|
|
306
|
+
# check for any bad options
|
|
307
|
+
unless (realopts.keys - OAI::Const::VERBS[verb]).empty?
|
|
308
|
+
raise OAI::ArgumentException.new
|
|
160
309
|
end
|
|
310
|
+
realopts
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def is_resumption?(opts)
|
|
314
|
+
if opts.keys.include?(:resumption_token)
|
|
315
|
+
return true if 1 == opts.keys.size
|
|
316
|
+
raise OAI::ArgumentException.new
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Convert our internal representations back into standard OAI options
|
|
321
|
+
def externalize(value)
|
|
322
|
+
value.to_s.gsub(/_[a-z]/) { |m| m.sub("_", '').capitalize }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def parse_date(value)
|
|
326
|
+
return value if value.respond_to?(:strftime)
|
|
327
|
+
|
|
328
|
+
Date.parse(value) # This will raise an exception for badly formatted dates
|
|
329
|
+
Time.parse(value).utc # Sadly, this will not
|
|
330
|
+
rescue
|
|
331
|
+
raise OAI::ArgumentException.new, "unparsable date: '#{value}'"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Strip out invalid UTF-8 characters. Regex from the W3C, inverted.
|
|
335
|
+
# http://www.w3.org/International/questions/qa-forms-utf-8.en.php
|
|
336
|
+
#
|
|
337
|
+
# Regex is from WebCollab:
|
|
338
|
+
# http://webcollab.sourceforge.net/unicode.html
|
|
339
|
+
def strip_invalid_utf_8_chars(xml)
|
|
340
|
+
return nil unless xml
|
|
341
|
+
|
|
342
|
+
# If it's in a specific encoding other than BINARY, it may trigger
|
|
343
|
+
# an exception to try to gsub these illegal bytes. Temporarily
|
|
344
|
+
# put it in BINARY. NOTE: We're not totally sure what's going on
|
|
345
|
+
# with encodings in this gem in general, it might not be totally reasonable.
|
|
346
|
+
orig_encoding = xml.encoding
|
|
347
|
+
xml.force_encoding("BINARY")
|
|
348
|
+
|
|
349
|
+
xml = xml.gsub(/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]
|
|
350
|
+
| [\x00-\x7F][\x80-\xBF]+
|
|
351
|
+
| ([\xC0\xC1]|[\xF0-\xFF])[\x80-\xBF]*
|
|
352
|
+
| [\xC2-\xDF]((?![\x80-\xBF])|[\x80-\xBF]{2,})
|
|
353
|
+
| [\xE0-\xEF](([\x80-\xBF](?![\x80-\xBF]))
|
|
354
|
+
| (?![\x80-\xBF]{2})|[\x80-\xBF]{3,})/x, '?')\
|
|
355
|
+
.gsub(/\xE0[\x80-\x9F][\x80-\xBF]
|
|
356
|
+
| \xED[\xA0-\xBF][\x80-\xBF]/,'?')
|
|
357
|
+
|
|
358
|
+
xml.force_encoding(orig_encoding)
|
|
359
|
+
|
|
360
|
+
xml
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def strip_invalid_xml_chars(xml)
|
|
364
|
+
return xml unless xml =~ UNESCAPED_AMPERSAND
|
|
365
|
+
xml.gsub(UNESCAPED_AMPERSAND, '&')
|
|
161
366
|
end
|
|
162
367
|
end
|
|
163
368
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module OAI
|
|
2
|
+
|
|
3
|
+
module Const
|
|
4
|
+
# OAI defines six verbs with various allowable options.
|
|
5
|
+
VERBS = {
|
|
6
|
+
'Identify' => [],
|
|
7
|
+
'ListMetadataFormats' => [:identifier],
|
|
8
|
+
'ListSets' => [:resumption_token], # unused currently
|
|
9
|
+
'GetRecord' => [:identifier, :from, :until, :set, :metadata_prefix],
|
|
10
|
+
'ListIdentifiers' => [:from, :until, :set, :metadata_prefix, :resumption_token],
|
|
11
|
+
'ListRecords' => [:from, :until, :set, :metadata_prefix, :resumption_token]
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
RESERVED_WORDS = %w{type id}
|
|
15
|
+
|
|
16
|
+
# Two granularities are supported in OIA-PMH, daily or seconds.
|
|
17
|
+
module Granularity
|
|
18
|
+
LOW = 'YYYY-MM-DD'
|
|
19
|
+
HIGH = 'YYYY-MM-DDThh:mm:ssZ'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Repositories can support three different schemes for dealing with deletions.
|
|
23
|
+
# * NO - No deletions allowed
|
|
24
|
+
# * TRANSIENT - Deletions are supported but may not be permanently maintained.
|
|
25
|
+
# * PERSISTENT - Deletions are supported and are permanently maintained.
|
|
26
|
+
module Delete
|
|
27
|
+
NO = :no
|
|
28
|
+
TRANSIENT = :transient
|
|
29
|
+
PERSISTENT = :persistent
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
data/lib/oai/exception.rb
CHANGED
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
module OAI
|
|
2
|
+
|
|
3
|
+
# Standard error responses for problems serving OAI content. These
|
|
4
|
+
# messages will be wrapped in an XML response to the client.
|
|
5
|
+
|
|
2
6
|
class Exception < RuntimeError
|
|
7
|
+
CODE = nil
|
|
8
|
+
MESSAGE = nil
|
|
9
|
+
|
|
10
|
+
attr_reader :code
|
|
11
|
+
|
|
12
|
+
@@codes = {}
|
|
13
|
+
|
|
14
|
+
def self.register_exception_code(code, exception_class)
|
|
15
|
+
@@codes[code] = exception_class if exception_class.superclass == OAI::Exception
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.for(message: nil, code: nil)
|
|
19
|
+
@@codes.fetch(code, Exception).new(message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(message = nil, code = nil)
|
|
23
|
+
super(message || self.class::MESSAGE)
|
|
24
|
+
@code = code || self.class::CODE
|
|
25
|
+
end
|
|
3
26
|
end
|
|
4
|
-
|
|
27
|
+
|
|
28
|
+
class ArgumentException < Exception
|
|
29
|
+
CODE = 'badArgument'
|
|
30
|
+
MESSAGE = 'The request includes ' \
|
|
31
|
+
'illegal arguments, is missing required arguments, includes a ' \
|
|
32
|
+
'repeated argument, or values for arguments have an illegal syntax.'
|
|
33
|
+
register_exception_code(CODE, self)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class VerbException < Exception
|
|
37
|
+
CODE = 'badVerb'
|
|
38
|
+
MESSAGE = 'Value of the verb argument is not a legal OAI-PMH '\
|
|
39
|
+
'verb, the verb argument is missing, or the verb argument is repeated.'
|
|
40
|
+
register_exception_code(CODE, self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class FormatException < Exception
|
|
44
|
+
CODE = 'cannotDisseminateFormat'
|
|
45
|
+
MESSAGE = 'The metadata format identified by '\
|
|
46
|
+
'the value given for the metadataPrefix argument is not supported '\
|
|
47
|
+
'by the item or by the repository.'
|
|
48
|
+
register_exception_code(CODE, self)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class IdException < Exception
|
|
52
|
+
CODE = 'idDoesNotExist'
|
|
53
|
+
MESSAGE = 'The value of the identifier argument is '\
|
|
54
|
+
'unknown or illegal in this repository.'
|
|
55
|
+
register_exception_code(CODE, self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class NoMatchException < Exception
|
|
59
|
+
CODE = 'noRecordsMatch'
|
|
60
|
+
MESSAGE = 'The combination of the values of the from, '\
|
|
61
|
+
'until, set and metadataPrefix arguments results in an empty list.'
|
|
62
|
+
register_exception_code(CODE, self)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class MetadataFormatException < Exception
|
|
66
|
+
CODE = 'noMetadataFormats'
|
|
67
|
+
MESSAGE = 'There are no metadata formats available '\
|
|
68
|
+
'for the specified item.'
|
|
69
|
+
register_exception_code(CODE, self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class SetException < Exception
|
|
73
|
+
CODE = 'noSetHierarchy'
|
|
74
|
+
MESSAGE = 'This repository does not support sets.'
|
|
75
|
+
register_exception_code(CODE, self)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class ResumptionTokenException < Exception
|
|
79
|
+
CODE = 'badResumptionToken'
|
|
80
|
+
MESSAGE = 'The value of the resumptionToken argument is invalid or expired.'
|
|
81
|
+
register_exception_code(CODE, self)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Created by William Groppe on 2006-11-05.
|
|
3
|
+
# Copyright (c) 2006. All rights reserved.
|
|
4
|
+
|
|
5
|
+
module OAI
|
|
6
|
+
module Harvester
|
|
7
|
+
|
|
8
|
+
LOW_RESOLUTION = OAI::Const::Granularity::LOW
|
|
9
|
+
|
|
10
|
+
class Config < OpenStruct
|
|
11
|
+
|
|
12
|
+
PERIODS = %w(daily weekly monthly)
|
|
13
|
+
GLOBAL = "/etc/oai/harvester.yml"
|
|
14
|
+
|
|
15
|
+
def self.load
|
|
16
|
+
config = find_config
|
|
17
|
+
File.exists?(config) ? new(YAML.load_file(config)) : new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def save
|
|
21
|
+
config = Config.find_config
|
|
22
|
+
open(config, 'w') do |out|
|
|
23
|
+
YAML.dump(@table, out)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
# Shamelessly lifted from Camping
|
|
29
|
+
def self.find_config
|
|
30
|
+
if home = ENV['HOME'] # POSIX
|
|
31
|
+
return GLOBAL if File.exists?(GLOBAL) && File.writable?(GLOBAL)
|
|
32
|
+
FileUtils.mkdir_p File.join(home, '.oai')
|
|
33
|
+
File.join(home, '.oai/harvester.yml')
|
|
34
|
+
elsif home = ENV['APPDATA'] # MSWIN
|
|
35
|
+
File.join(home, 'oai/harvester.yml')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|