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
@@ -0,0 +1,474 @@
1
+ #!/usr/local/bin/ruby -rubygems
2
+ require 'camping'
3
+ require 'camping/session'
4
+ require 'oai/provider'
5
+
6
+ # Extremely simple demo Camping application to illustrate OAI Provider integration
7
+ # with Camping.
8
+ #
9
+ # William Groppe 2/1/2007
10
+ #
11
+
12
+ Camping.goes :DublinCore
13
+
14
+ module DublinCore
15
+ include Camping::Session
16
+
17
+ FIELDS = ['title', 'creator', 'subject', 'description',
18
+ 'publisher', 'contributor', 'date', 'type', 'format',
19
+ 'identifier', 'source', 'language', 'relation', 'coverage', 'rights']
20
+
21
+ def DublinCore.create
22
+ Camping::Models::Session.create_schema
23
+ DublinCore::Models.create_schema :assume =>
24
+ (DublinCore::Models::Obj.table_exists? ? 1.0 : 0.0)
25
+ end
26
+
27
+ end
28
+
29
+ module DublinCore::Models
30
+ Base.logger = Logger.new("dublin_core.log")
31
+ Base.inheritance_column = 'field_type'
32
+ Base.default_timezone = :utc
33
+
34
+ class Obj < Base # since Object is reserved
35
+ has_and_belongs_to_many :fields, :join_table => 'dublincore_field_links',
36
+ :foreign_key => 'obj_id', :association_foreign_key => 'field_id'
37
+ DublinCore::FIELDS.each do |field|
38
+ class_eval(%{
39
+ def #{field.pluralize}
40
+ fields.select do |f|
41
+ f if f.field_type == "DC#{field.capitalize}"
42
+ end
43
+ end
44
+ });
45
+ end
46
+ end
47
+
48
+ class Field < Base
49
+ has_and_belongs_to_many :objs, :join_table => 'dublincore_field_links',
50
+ :foreign_key => 'field_id', :association_foreign_key => 'obj_id'
51
+ validates_presence_of :field_type, :message => "can't be blank"
52
+
53
+ # Support sorting by value
54
+ def <=>(other)
55
+ self.to_s <=> other.to_s
56
+ end
57
+
58
+ def to_s
59
+ value
60
+ end
61
+ end
62
+
63
+ DublinCore::FIELDS.each do |field|
64
+ module_eval(%{
65
+ class DC#{field.capitalize} < Field; end
66
+ })
67
+ end
68
+
69
+ # OAI Provider configuration
70
+ class CampingProvider < OAI::Provider::Base
71
+ repository_name 'Camping Test OAI Repository'
72
+ source_model ActiveRecordWrapper.new(Obj)
73
+ end
74
+
75
+ class CreateTheBasics < V 1.0
76
+ def self.up
77
+ create_table :dublincore_objs, :force => true do |t|
78
+ t.column :source, :string
79
+ t.column :created_at, :datetime
80
+ t.column :updated_at, :datetime
81
+ end
82
+
83
+ create_table :dublincore_field_links, :id => false, :force => true do |t|
84
+ t.column :obj_id, :integer, :null => false
85
+ t.column :field_id, :integer, :null => false
86
+ end
87
+
88
+ create_table :dublincore_fields, :force => true do |t|
89
+ t.column :field_type, :string, :limit => 30, :null => false
90
+ t.column :value, :text, :null => false
91
+ end
92
+
93
+ add_index :dublincore_fields, [:field_type, :value], :uniq => true
94
+ add_index :dublincore_field_links, :field_id
95
+ add_index :dublincore_field_links, [:obj_id, :field_id]
96
+ end
97
+
98
+ def self.down
99
+ drop_table :dublincore_objs
100
+ drop_table :dublincore_field_links
101
+ drop_table :dublincore_fields
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ module DublinCore::Controllers
108
+
109
+ # Now setup a URL('/oai' by default) to handle OAI requests
110
+ class Oai
111
+ def get
112
+ @headers['Content-Type'] = 'text/xml'
113
+ provider = Models::CampingProvider.new
114
+ provider.process_request(@input.merge(:url => "http:"+URL(Oai).to_s))
115
+ end
116
+ end
117
+
118
+ class Index < R '/', '/browse/(\w+)', '/browse/(\w+)/page/(\d+)'
119
+ def get(field = nil, page = 1)
120
+ @field = field
121
+ @page = page.to_i
122
+ @browse = {}
123
+ if !@field
124
+ FIELDS.each do |field|
125
+ @browse[field] = Field.count(
126
+ :conditions => ["field_type = ?", "DC#{field.capitalize}"])
127
+ end
128
+ @home = true
129
+ @count = @browse.keys.size
130
+ else
131
+ @count = Field.count(:conditions => ["field_type = ?", "DC#{@field.capitalize}"])
132
+ fields = Field.find(:all,
133
+ :conditions => ["field_type = ?", "DC#{@field.capitalize}"],
134
+ :order => "value asc", :limit => DublinCore::LIMIT,
135
+ :offset => (@page - 1) * DublinCore::LIMIT)
136
+
137
+ fields.each do |field|
138
+ @browse[field] = field.objs.size
139
+ end
140
+ end
141
+ render :browse
142
+ end
143
+ end
144
+
145
+ class Search < R '/search', '/search/page/(\d+)'
146
+
147
+ def get(page = 1)
148
+ @page = page.to_i
149
+ if input.terms
150
+ @state.terms = input.terms if input.terms
151
+
152
+ start = Time.now
153
+ ids = search(input.terms, @page - 1)
154
+ finish = Time.now
155
+ @search_time = (finish - start)
156
+ @objs = Obj.find(ids)
157
+ else
158
+ @count = 0
159
+ @objs = []
160
+ end
161
+
162
+ render :search
163
+ end
164
+
165
+ end
166
+
167
+ class LinkedTo < R '/linked/(\d+)', '/linked/(\d+)/page/(\d+)'
168
+ def get(field, page = 1)
169
+ @page = page.to_i
170
+ @field = field
171
+ @count = Field.find(field).objs.size
172
+ @objs = Field.find(field).objs.find(:all,
173
+ :limit => DublinCore::LIMIT,
174
+ :offset => (@page - 1) * DublinCore::LIMIT)
175
+ render :records
176
+ end
177
+ end
178
+
179
+ class Add
180
+ def get
181
+ @obj = Obj.create
182
+ render :edit
183
+ end
184
+ end
185
+
186
+ class View < R '/view/(\d+)'
187
+ def get obj_id
188
+ obj = Obj.find(obj_id)
189
+ # Get rid of completely empty records
190
+ obj.destroy if obj.fields.empty?
191
+
192
+ @count = 1
193
+ @objs = [obj]
194
+ if Obj.exists?(obj.id)
195
+ render :records if Obj.exists?(obj.id)
196
+ else
197
+ redirect Index
198
+ end
199
+ end
200
+ end
201
+
202
+ class Edit < R '/edit', '/edit/(\d+)'
203
+ def get obj_id
204
+ @obj = Obj.find obj_id
205
+ render :edit
206
+ end
207
+
208
+ def post
209
+ case input.action
210
+ when 'Save'
211
+ @obj = Obj.find input.obj_id
212
+ @obj.fields.clear
213
+ input.keys.each do |key|
214
+ next unless key =~ /^DublinCore::Models::\w+/
215
+ next unless input[key] && !input[key].empty?
216
+ input[key].to_a.each do |value|
217
+ @obj.fields << key.constantize.find_or_create_by_value(value)
218
+ end
219
+ end
220
+ redirect View, @obj
221
+ when 'Discard'
222
+ @obj = Obj.find input.obj_id
223
+
224
+ # Get rid of completely empty records
225
+ @obj.destroy if @obj.fields.empty?
226
+
227
+ if Obj.exists?(@obj.id)
228
+ redirect View, @obj
229
+ else
230
+ redirect Index
231
+ end
232
+ when 'Delete'
233
+ Obj.find(input.obj_id).destroy
234
+ render :delete_success
235
+ end
236
+ end
237
+ end
238
+
239
+ class DataAdd < R '/data/add'
240
+ def post
241
+ if input.field_value && !input.field_value.empty?
242
+ model = "DublinCore::Models::#{input.field_type}".constantize
243
+ obj = Obj.find(input.obj_id)
244
+ obj.fields << model.find_or_create_by_value(input.field_value)
245
+ end
246
+ redirect Edit, input.obj_id
247
+ end
248
+ end
249
+
250
+ class Style < R '/styles.css'
251
+ def get
252
+ @headers["Content-Type"] = "text/css; charset=utf-8"
253
+ @body = %{
254
+ body { width: 750px; margin: 0; margin-left: auto; margin-right: auto; padding: 0;
255
+ color: black; background-color: white; }
256
+ a { color: #CC6600; text-decoration: none; }
257
+ a:visited { color: #CC6600; text-decoration: none;}
258
+ a:hover { text-decoration: underline; }
259
+ a.stealthy { color: black; }
260
+ a.stealthy:visited { color: black; }
261
+ .header { text-align: right; padding-right: .5em; }
262
+ div.search { text-align: right; position: relative; top: -1em; }
263
+ div.search form input { margin-right: .25em; }
264
+ .small { font-size: 70%; }
265
+ .tiny { font-size: 60%; }
266
+ .totals { font-size: 60%; margin-left: .25em; vertical-align: super; }
267
+ .field_labels { font-size: 60%; margin-left: 1em; vertical-align: super; }
268
+ h2 {color: #CC6600; padding: 0; margin-bottom: .15em; font-size: 160%;}
269
+ h3.header { padding:0; margin:0; position: relative; top: -2.8em;
270
+ padding-bottom: .25em; padding-right: 5em; font-size: 80%; }
271
+ h1.header a { color: #FF9900; text-decoration: none;
272
+ font: bold 250% "Trebuchet MS",Trebuchet,Georgia, Serif;
273
+ letter-spacing:-4px; }
274
+
275
+ div.pagination { text-align: center; }
276
+ ul.pages { list-style: none; padding: 0; display: inline;}
277
+ ul.pages li { display: inline; }
278
+ form.controls { text-align: right; }
279
+ ul.undecorated { list-style: none; padding-left: 1em; margin-bottom: 5em;}
280
+ .content { padding-left: 2em; padding-right: 2em; }
281
+ table { padding: 0; background-color: #CCEECC; font-size: 75%;
282
+ width: 100%; border: 1px solid black; margin: 1em; margin-left: auto; margin-right: auto; }
283
+ table.obj tr.controls { text-align: right; font-size: 100%; background-color: #AACCAA; }
284
+ table.obj td.label { width: 7em; padding-left: .25em; border-right: 1px solid black; }
285
+ table.obj td.value input { width: 80%; margin: .35em; }
286
+ input.button { width: 5em; margin-left: .5em; }
287
+ table.add tr.controls td { padding: .5em; font-size: 100%; background-color: #AACCAA; }
288
+ table.add td { width: 10%; }
289
+ table.add td.value { width: 80%; }
290
+ table.add td.value input { width: 100%; margin: .35em; }
291
+ }
292
+ end
293
+ end
294
+ end
295
+
296
+ module DublinCore::Helpers
297
+
298
+ def paginate(klass, term = nil)
299
+ @total_pages = count/DublinCore::LIMIT + 1
300
+ div.pagination do
301
+ p "#{@page} of #{@total_pages} pages"
302
+ ul.pages do
303
+ li { link_if("<<", klass, term, 1) }
304
+ li { link_if("<", klass, term, @page - 1) }
305
+ page_window.each do |page|
306
+ li { link_if("#{page}", klass, term, page) }
307
+ end
308
+ li { link_if(">", klass, term, @page + 1) }
309
+ li { link_if(">>", klass, term, @total_pages) }
310
+ end
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def link_if(string, klass, term, page)
317
+ return "#{string} " if (@page == page || 1 > page || page > @total_pages)
318
+ a(string, :href => term.nil? ? R(klass, page) : R(klass, term, page)) << " "
319
+ end
320
+
321
+ def page_window
322
+ return 1..@total_pages if @total_pages < 9
323
+ size = @total_pages > 9 ? 9 : @total_pages
324
+ start = @page - size/2 > 0 ? @page - size/2 : 1
325
+ start = @total_pages - size if start+size > @total_pages
326
+ start..start+size
327
+ end
328
+
329
+ end
330
+
331
+ module DublinCore::Views
332
+
333
+ def layout
334
+ html do
335
+ head do
336
+ title "Dublin Core - Simple Asset Cataloger"
337
+ link :rel => 'stylesheet', :type => 'text/css',
338
+ :href => '/styles.css', :media => 'screen'
339
+ end
340
+ body do
341
+ h1.header { a 'Nugget Explorer', :href => R(Index) }
342
+ h3.header { "exposing ugly metadata" }
343
+ div.search do
344
+ form({:method => 'get', :action => R(Search)}) do
345
+ input :name => 'terms', :type => 'text'
346
+ input.button :type => :submit, :value => 'Search'
347
+ end
348
+ end
349
+ a("Home", :href => R(Index)) unless @home
350
+ div.content do
351
+ self << yield
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ def browse
358
+ if @browse.empty?
359
+ p 'No objects found, try adding one.'
360
+ else
361
+ h3 "Browsing" << (" '#{@field}'" if @field).to_s
362
+ ul.undecorated do
363
+ @browse.keys.sort.each do |key|
364
+ li { _key_value(key, @browse[key]) }
365
+ end
366
+ end
367
+ paginate(Index, @field) if @count > DublinCore::LIMIT
368
+ end
369
+ end
370
+
371
+ def delete_success
372
+ p "Delete was successful"
373
+ end
374
+
375
+ def search
376
+ p.results { span "#{count} results for '#{@state.terms}'"; span.tiny "(#{@search_time} secs)" }
377
+ ul.undecorated do
378
+ @result.keys.sort.each do |record|
379
+ li do
380
+ a(record.value, :href => R(LinkedTo, record.id))
381
+ span.totals "(#{@result[record]})"
382
+ span.field_labels "#{record.field_type.sub(/^DC/, '').downcase} "
383
+ end
384
+ end
385
+ end
386
+ paginate(Search) if @count > DublinCore::LIMIT
387
+ end
388
+
389
+ def edit
390
+ h3 "Editing Record"
391
+ p "To remove a field entry, just remove it's content."
392
+ _form(@obj, :action => R(Edit, @obj))
393
+ end
394
+
395
+ def records
396
+ @objs.each { |obj| _obj(obj) }
397
+ paginate(LinkedTo, @field) if @count > DublinCore::LIMIT
398
+ end
399
+
400
+ def _obj(obj, edit = false)
401
+ table.obj(:cellspacing => 0) do
402
+ _edit_controls(obj, edit)
403
+ DublinCore::FIELDS.each do |field|
404
+ obj.send(field.pluralize.intern).each_with_index do |value, index|
405
+ tr do
406
+ td.label { 0 == index ? "#{field}(s)" : "&nbsp;" }
407
+ if edit
408
+ td.value do
409
+ input :name => value.class,
410
+ :type => 'text',
411
+ :value => value.to_s
412
+ end
413
+ else
414
+ td.value { a.stealthy(value, :href => R(LinkedTo, value.id)) }
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ def _form(obj, action)
423
+ form.controls(:method => 'post', :action => R(Edit)) do
424
+ input :type => 'hidden', :name => 'obj_id', :value => obj.id
425
+ _obj(obj, true)
426
+ input.button :type => :submit, :name => 'action', :value => 'Save'
427
+ input.button :type => :submit, :name => 'action', :value => 'Discard'
428
+ end
429
+ form(:method => 'post', :action => R(DataAdd)) do
430
+ input :type => 'hidden', :name => 'obj_id', :value => obj.id
431
+ table.add :cellspacing => 0 do
432
+ tr.controls do
433
+ td(:colspan => 3) { "Add an entry. (All changes above will be lost, so save them first)" }
434
+ end
435
+ tr do
436
+ td do
437
+ select(:name => 'field_type') do
438
+ DublinCore::FIELDS.each do |field|
439
+ option field, :value => "DC#{field.capitalize}"
440
+ end
441
+ end
442
+ end
443
+ td.value { input :name => 'field_value', :type => 'text' }
444
+ td { input.button :type => 'submit', :value => 'Add' }
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ def _edit_controls(obj, edit)
451
+ tr.controls do
452
+ td :colspan => 2 do
453
+ edit ? input(:type => 'submit', :name => 'action', :value => 'Delete') :
454
+ a('edit', :href => R(Edit, obj))
455
+ end
456
+ end
457
+ end
458
+
459
+
460
+ def _key_value(key, value)
461
+ if value > 0
462
+ if key.kind_of?(DublinCore::Models::Field)
463
+ a(key, :href => R(LinkedTo, key.id))
464
+ else
465
+ a(key.to_s, :href => R(Index, key))
466
+ end
467
+ span.totals "(#{value})"
468
+ else
469
+ span key
470
+ span.totals "(#{value})"
471
+ end
472
+ end
473
+
474
+ end
@@ -5,7 +5,11 @@ module OAI
5
5
 
6
6
  def initialize(doc)
7
7
  super doc
8
- @record = OAI::Record.new(xpath_first(doc, './/record'))
8
+ @record = OAI::Record.new(xpath_first(doc, './/GetRecord/record'))
9
+ end
10
+
11
+ def deleted?
12
+ return @record.deleted?
9
13
  end
10
14
  end
11
15
  end
@@ -0,0 +1,18 @@
1
+ module OAI
2
+ class Header
3
+ include OAI::XPath
4
+ attr_accessor :status, :identifier, :datestamp, :set_spec
5
+
6
+ def initialize(element)
7
+ @status = get_attribute(element, 'status')
8
+ @identifier = xpath(element, './/identifier')
9
+ @datestamp = xpath(element, './/datestamp')
10
+ @set_spec = xpath_all(element, './/setSpec')
11
+ end
12
+
13
+ def deleted?
14
+ return true if @status.to_s == "deleted"
15
+ end
16
+
17
+ end
18
+ end
@@ -21,6 +21,8 @@ module OAI
21
21
  end
22
22
 
23
23
  # returns REXML::Element nodes for each description section
24
+ # if the OAI::Client was configured to use libxml then you will
25
+ # instead get a LibXML::XML::Node object.
24
26
  def descriptions
25
27
  return xpath_all(doc, './/Identify/description')
26
28
  end
@@ -1,6 +1,7 @@
1
1
  module OAI
2
2
  class ListIdentifiersResponse < Response
3
3
  include Enumerable
4
+ include OAI::Resumable
4
5
  include OAI::XPath
5
6
 
6
7
  def each
@@ -2,15 +2,16 @@ module OAI
2
2
 
3
3
  # allows for iteration across a list of records
4
4
  #
5
- # for record in client.list_records :metadata_prefix => 'oai_dc':
6
- # puts record.metadata
7
- # end
5
+ # client.list_records(:metadata_prefix => 'oai_dc').each do |record|
6
+ # puts record.metadata
7
+ # end
8
8
  #
9
9
  # you'll need to handle resumption tokens
10
-
10
+
11
11
  class ListRecordsResponse < Response
12
- include OAI::XPath
13
12
  include Enumerable
13
+ include OAI::Resumable
14
+ include OAI::XPath
14
15
 
15
16
  def each
16
17
  for record_element in xpath_all(@doc, './/ListRecords/record')
@@ -2,17 +2,18 @@ module OAI
2
2
 
3
3
  # allows for iteration of the sets found in a oai-pmh server
4
4
  #
5
- # for set in client.list_sets
6
- # puts set
7
- # end
8
-
5
+ # for set in client.list_sets
6
+ # puts set
7
+ # end
8
+
9
9
  class ListSetsResponse < Response
10
- include OAI::XPath
11
10
  include Enumerable
11
+ include OAI::Resumable
12
+ include OAI::XPath
12
13
 
13
14
  def each
14
15
  for set_element in xpath_all(@doc, './/set')
15
- yield OAI::Set.new(set_element)
16
+ yield OAI::Set.parse(set_element)
16
17
  end
17
18
  end
18
19
  end
@@ -0,0 +1,27 @@
1
+ module OAI
2
+
3
+ # A class for representing a Record as returned from a `GetRecord`
4
+ # or `ListRecords` request. Each record will have a header and metadata
5
+ # attribute. The header is a {OAI::Header} object and the metadata is
6
+ # a `REXML::Element` object for that chunk of XML.
7
+ #
8
+ # Note: if your {OAI::Client} was configured to use the 'libxml' parser
9
+ # metadata will return a `XML::Node` object instead.
10
+ class Record
11
+ include OAI::XPath
12
+ attr_accessor :header, :metadata, :about, :_source
13
+
14
+ def initialize(element)
15
+ @_source = element
16
+ @header = OAI::Header.new xpath_first(element, './/header')
17
+ @metadata = xpath_first(element, './/metadata')
18
+ @about = xpath_first(element, './/about')
19
+ end
20
+
21
+ # a convenience method which digs into the header status attribute
22
+ # and returns true if the value is set to 'deleted'
23
+ def deleted?
24
+ return @header.deleted?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,53 @@
1
+ module OAI
2
+
3
+ # An OAI::Response contains entries and a resumption token. If a resumption token is present,
4
+ # then you must use it to fetch the rest of the entries for your query. For example:
5
+ #
6
+ # ```ruby
7
+ # # List all records in a given set
8
+ # client = OAI::Client.new 'http://my-oai-provider.example.com/oai'
9
+ # response = client.list_records :set => 'my_set_name'
10
+ # while response.entries.count > 0
11
+ # response.entries.each { |entry|
12
+ # puts entry.header.identifier
13
+ # }
14
+ # token = response.resumption_token
15
+ # # Note: You do not need to pass the options hash again, just the verb and the resumption token
16
+ # response = client.list_records :resumption_token => token if token
17
+ # end
18
+ # ```
19
+ class Response
20
+ include OAI::XPath
21
+ attr_reader :doc, :resumption_token, :resumption_block
22
+
23
+ def initialize(doc, &resumption_block)
24
+ @doc = doc
25
+ @resumption_token = xpath(doc, './/resumptionToken')
26
+ @resumption_block = resumption_block
27
+
28
+ # throw an exception if there was an error
29
+ error = xpath_first(doc, './/error')
30
+ return unless error
31
+
32
+ case error.class.to_s
33
+ when 'REXML::Element'
34
+ message = error.text
35
+ code = error.attributes['code']
36
+ when 'LibXML::XML::Node'
37
+ message = error.content
38
+ code = ""
39
+ if defined?(error.property) == nil
40
+ code = error.attributes['code']
41
+ else
42
+ begin
43
+ code = error["code"]
44
+ rescue
45
+ code = error.property('code')
46
+ end
47
+ end
48
+ end
49
+ raise OAI::Exception.for(message: message, code: code)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ module OAI
2
+ module Resumable
3
+
4
+ class ResumptionWrapper
5
+ include Enumerable
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ @resumption_block = response.resumption_block
10
+ end
11
+
12
+ def each(&block)
13
+ yield_from_response &block
14
+ while resumable?
15
+ @response = @resumption_block.call @response
16
+ yield_from_response &block
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def yield_from_response(&block)
23
+ @response.each do |obj|
24
+ block.call(obj)
25
+ end
26
+ end
27
+
28
+ def resumable?
29
+ @response.resumption_token and not @response.resumption_token.empty?
30
+ end
31
+
32
+ end
33
+
34
+ def full
35
+ if @resumption_block.nil?
36
+ raise NotImplementedError.new("Resumption block not provided")
37
+ end
38
+ ResumptionWrapper.new(self)
39
+ end
40
+
41
+ end
42
+ end