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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +128 -0
  3. data/Rakefile +64 -0
  4. data/bin/oai +66 -0
  5. data/examples/models/file_model.rb +63 -0
  6. data/examples/providers/dublin_core.rb +474 -0
  7. data/lib/oai/{get_record.rb → client/get_record.rb} +5 -1
  8. data/lib/oai/client/header.rb +18 -0
  9. data/lib/oai/{identify.rb → client/identify.rb} +2 -0
  10. data/lib/oai/{list_identifiers.rb → client/list_identifiers.rb} +1 -0
  11. data/lib/oai/{list_records.rb → client/list_records.rb} +6 -5
  12. data/lib/oai/{list_sets.rb → client/list_sets.rb} +7 -6
  13. data/lib/oai/client/record.rb +27 -0
  14. data/lib/oai/client/response.rb +53 -0
  15. data/lib/oai/client/resumable.rb +42 -0
  16. data/lib/oai/client.rb +313 -108
  17. data/lib/oai/constants.rb +34 -0
  18. data/lib/oai/exception.rb +80 -1
  19. data/lib/oai/harvester/config.rb +41 -0
  20. data/lib/oai/harvester/harvest.rb +162 -0
  21. data/lib/oai/harvester/logging.rb +68 -0
  22. data/lib/oai/harvester/mailer.rb +17 -0
  23. data/lib/oai/harvester/shell.rb +337 -0
  24. data/lib/oai/harvester.rb +43 -0
  25. data/lib/oai/provider/metadata_format/oai_dc.rb +28 -0
  26. data/lib/oai/provider/metadata_format.rb +119 -0
  27. data/lib/oai/provider/model/activerecord_caching_wrapper.rb +129 -0
  28. data/lib/oai/provider/model/activerecord_wrapper.rb +217 -0
  29. data/lib/oai/provider/model.rb +80 -0
  30. data/lib/oai/provider/partial_result.rb +18 -0
  31. data/lib/oai/provider/response/error.rb +16 -0
  32. data/lib/oai/provider/response/get_record.rb +27 -0
  33. data/lib/oai/provider/response/identify.rb +37 -0
  34. data/lib/oai/provider/response/list_identifiers.rb +30 -0
  35. data/lib/oai/provider/response/list_metadata_formats.rb +39 -0
  36. data/lib/oai/provider/response/list_records.rb +47 -0
  37. data/lib/oai/provider/response/list_sets.rb +23 -0
  38. data/lib/oai/provider/response/record_response.rb +103 -0
  39. data/lib/oai/provider/response.rb +133 -0
  40. data/lib/oai/provider/resumption_token.rb +155 -0
  41. data/lib/oai/provider.rb +487 -0
  42. data/lib/oai/set.rb +14 -5
  43. data/lib/oai/xpath.rb +59 -5
  44. data/lib/oai.rb +7 -13
  45. data/lib/test.rb +25 -0
  46. data/test/activerecord_provider/config/connection.rb +30 -0
  47. data/test/activerecord_provider/database/0001_oaipmh_tables.rb +70 -0
  48. data/test/activerecord_provider/fixtures/dc.yml +1501 -0
  49. data/test/activerecord_provider/helpers/providers.rb +68 -0
  50. data/test/activerecord_provider/helpers/set_provider.rb +24 -0
  51. data/test/activerecord_provider/helpers/transactional_test_case.rb +35 -0
  52. data/test/activerecord_provider/models/dc_field.rb +15 -0
  53. data/test/activerecord_provider/models/dc_lang.rb +3 -0
  54. data/test/activerecord_provider/models/dc_set.rb +7 -0
  55. data/test/activerecord_provider/models/exclusive_set_dc_field.rb +17 -0
  56. data/test/activerecord_provider/models/oai_token.rb +3 -0
  57. data/test/activerecord_provider/tc_activerecord_wrapper.rb +63 -0
  58. data/test/activerecord_provider/tc_ar_provider.rb +199 -0
  59. data/test/activerecord_provider/tc_ar_sets_provider.rb +159 -0
  60. data/test/activerecord_provider/tc_caching_paging_provider.rb +50 -0
  61. data/test/activerecord_provider/tc_simple_paging_provider.rb +71 -0
  62. data/test/activerecord_provider/test_helper_ar_provider.rb +7 -0
  63. data/test/client/helpers/provider.rb +65 -0
  64. data/test/client/helpers/test_wrapper.rb +6 -0
  65. data/test/client/tc_exception.rb +36 -0
  66. data/test/client/tc_get_record.rb +39 -0
  67. data/test/client/tc_http_client.rb +129 -0
  68. data/test/client/tc_identify.rb +13 -0
  69. data/test/client/tc_libxml.rb +61 -0
  70. data/test/{tc_list_identifiers.rb → client/tc_list_identifiers.rb} +28 -9
  71. data/test/{tc_list_metadata_formats.rb → client/tc_list_metadata_formats.rb} +4 -1
  72. data/test/client/tc_list_records.rb +26 -0
  73. data/test/client/tc_list_sets.rb +34 -0
  74. data/test/client/tc_low_resolution_dates.rb +14 -0
  75. data/test/client/tc_utf8_escaping.rb +19 -0
  76. data/test/client/tc_xpath.rb +26 -0
  77. data/test/client/test_helper_client.rb +5 -0
  78. data/test/harvester/tc_harvest.rb +42 -0
  79. data/test/harvester/test_helper_harvester.rb +6 -0
  80. data/test/provider/models.rb +246 -0
  81. data/test/provider/tc_exceptions.rb +81 -0
  82. data/test/provider/tc_functional_tokens.rb +53 -0
  83. data/test/provider/tc_instance_provider.rb +41 -0
  84. data/test/provider/tc_provider.rb +136 -0
  85. data/test/provider/tc_resumption_tokens.rb +52 -0
  86. data/test/provider/tc_simple_provider.rb +138 -0
  87. data/test/provider/test_helper_provider.rb +62 -0
  88. data/test/test.xml +22 -0
  89. metadata +238 -56
  90. data/lib/oai/header.rb +0 -12
  91. data/lib/oai/record.rb +0 -11
  92. data/lib/oai/response.rb +0 -20
  93. data/test/tc_get_record.rb +0 -27
  94. data/test/tc_identify.rb +0 -8
  95. data/test/tc_list_records.rb +0 -9
  96. data/test/tc_list_sets.rb +0 -17
  97. data/test.rb +0 -12
  98. /data/lib/oai/{list_metadata_formats.rb → client/list_metadata_formats.rb} +0 -0
  99. /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 'net/http'
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
- # client = OAI::Harvester.new ''http://www.pubmedcentral.gov/oai/oai.cgi'
13
- # client.list_identifiers :metadata_prefix => 'oai_dc'
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 api uses methods and parameter names with
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
- # The constructor which must be passed a valid base url for an oai
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
- # client = OAI::Harvseter.new 'http://www.pubmedcentral.gov/oai/oai.cgi'
35
-
36
- def initialize(base_url)
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. You'll get back a OAI::IdentifyResponse
41
- # object which is essentially just a wrapper around a REXML::Document
42
- # for the response.
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
- return IdentifyResponse.new(do_request(:verb => 'Identify'))
129
+ OAI::IdentifyResponse.new(do_request('Identify'))
46
130
  end
47
131
 
48
- # Equivalent to a ListMetadataFormats request. A ListMetadataFormatsResponse
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
- opts[:verb] = 'ListMetadataFormats'
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 :from, :until arguments
58
- # as Date or DateTime objects as appropriate depending on the granularity
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
- opts[:verb] = 'ListIdentifiers'
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
- opts[:verb] = 'GetRecord'
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
- # for record in client.list_records
83
- # puts record.metadata
84
- # end
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
- opts[:verb] = 'ListRecords'
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
- # for set in client.list_sets
99
- # puts set
100
- # end
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
- opts[:verb] = 'ListSets'
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
- private
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
- def do_request(hash)
111
- uri = @base.clone
214
+ private
112
215
 
113
- # build up the query string
114
- parts = hash.entries.map do |entry|
115
- key = studly(entry[0].to_s)
116
- value = entry[1]
117
- # dates get stringified using ISO8601, strings are url encoded
118
- value = case value
119
- when DateTime then value.strftime('%Y-%m-%dT%H:%M:%SZ');
120
- when Date then value.strftime('%Y-%m-%d')
121
- else CGI.escape(entry[1].to_s)
122
- end
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
- uri.query = parts.join('&')
126
-
127
- # fire off the request and return an REXML::Document object
128
- begin
129
- xml = Net::HTTP.get(uri)
130
- return REXML::Document.new(xml)
131
- rescue
132
- raise OAI::Exception, 'error during oai operation', caller
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
- # convert foo_bar to fooBar thus allowing our ruby code to use
137
- # the typical underscore idiom
138
- def studly(s)
139
- s.gsub(/_(\w)/) do |match|
140
- match.sub! '_', ''
141
- match.upcase
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
- # add a metadata prefix unless it's there or we are working with
146
- # a resumption token, and having one added could cause problems
147
- def add_default_metadata_prefix(opts)
148
- unless opts.has_key? :metadata_prefix or opts.has_key? :resumption_token
149
- opts[:metadata_prefix] = 'oai_dc'
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 verify_verb_arguments(opts, valid_opts)
154
- opts.keys.each do |opt|
155
- if opt =~ /[A-Z]/
156
- raise OAI::Exception.new("#{opt} should use underscores")
157
- elsif not valid_opts.include? opt
158
- raise OAI::Exception.new("invalid option #{opt} in #{opts['verb']}")
159
- end
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, '&amp;')
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
- end
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