oai_talia 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/README +81 -0
  2. data/Rakefile +127 -0
  3. data/bin/oai +68 -0
  4. data/examples/models/file_model.rb +63 -0
  5. data/examples/providers/dublin_core.rb +474 -0
  6. data/lib/oai/client/get_record.rb +15 -0
  7. data/lib/oai/client/header.rb +18 -0
  8. data/lib/oai/client/identify.rb +30 -0
  9. data/lib/oai/client/list_identifiers.rb +12 -0
  10. data/lib/oai/client/list_metadata_formats.rb +12 -0
  11. data/lib/oai/client/list_records.rb +21 -0
  12. data/lib/oai/client/list_sets.rb +19 -0
  13. data/lib/oai/client/metadata_format.rb +12 -0
  14. data/lib/oai/client/record.rb +26 -0
  15. data/lib/oai/client/response.rb +35 -0
  16. data/lib/oai/client.rb +301 -0
  17. data/lib/oai/constants.rb +34 -0
  18. data/lib/oai/exception.rb +75 -0
  19. data/lib/oai/harvester/config.rb +41 -0
  20. data/lib/oai/harvester/harvest.rb +150 -0
  21. data/lib/oai/harvester/logging.rb +70 -0
  22. data/lib/oai/harvester/mailer.rb +17 -0
  23. data/lib/oai/harvester/shell.rb +338 -0
  24. data/lib/oai/harvester.rb +39 -0
  25. data/lib/oai/provider/metadata_format/oai_dc.rb +29 -0
  26. data/lib/oai/provider/metadata_format/oai_europeana.rb +38 -0
  27. data/lib/oai/provider/metadata_format.rb +143 -0
  28. data/lib/oai/provider/model/activerecord_caching_wrapper.rb +134 -0
  29. data/lib/oai/provider/model/activerecord_wrapper.rb +139 -0
  30. data/lib/oai/provider/model.rb +74 -0
  31. data/lib/oai/provider/partial_result.rb +18 -0
  32. data/lib/oai/provider/response/error.rb +16 -0
  33. data/lib/oai/provider/response/get_record.rb +26 -0
  34. data/lib/oai/provider/response/identify.rb +25 -0
  35. data/lib/oai/provider/response/list_identifiers.rb +35 -0
  36. data/lib/oai/provider/response/list_metadata_formats.rb +34 -0
  37. data/lib/oai/provider/response/list_records.rb +34 -0
  38. data/lib/oai/provider/response/list_sets.rb +23 -0
  39. data/lib/oai/provider/response/record_response.rb +70 -0
  40. data/lib/oai/provider/response.rb +161 -0
  41. data/lib/oai/provider/resumption_token.rb +106 -0
  42. data/lib/oai/provider.rb +304 -0
  43. data/lib/oai/set.rb +29 -0
  44. data/lib/oai/xpath.rb +75 -0
  45. data/lib/oai.rb +8 -0
  46. data/lib/test.rb +25 -0
  47. data/test/activerecord_provider/config/connection.rb +5 -0
  48. data/test/activerecord_provider/config/database.yml +6 -0
  49. data/test/activerecord_provider/database/ar_migration.rb +59 -0
  50. data/test/activerecord_provider/database/oaipmhtest +0 -0
  51. data/test/activerecord_provider/fixtures/dc.yml +1501 -0
  52. data/test/activerecord_provider/helpers/providers.rb +44 -0
  53. data/test/activerecord_provider/helpers/set_provider.rb +36 -0
  54. data/test/activerecord_provider/models/dc_field.rb +7 -0
  55. data/test/activerecord_provider/models/dc_set.rb +6 -0
  56. data/test/activerecord_provider/models/oai_token.rb +3 -0
  57. data/test/activerecord_provider/tc_ar_provider.rb +113 -0
  58. data/test/activerecord_provider/tc_ar_sets_provider.rb +72 -0
  59. data/test/activerecord_provider/tc_caching_paging_provider.rb +55 -0
  60. data/test/activerecord_provider/tc_simple_paging_provider.rb +57 -0
  61. data/test/activerecord_provider/test_helper.rb +4 -0
  62. data/test/client/helpers/provider.rb +68 -0
  63. data/test/client/helpers/test_wrapper.rb +11 -0
  64. data/test/client/tc_exception.rb +36 -0
  65. data/test/client/tc_get_record.rb +37 -0
  66. data/test/client/tc_identify.rb +13 -0
  67. data/test/client/tc_libxml.rb +61 -0
  68. data/test/client/tc_list_identifiers.rb +52 -0
  69. data/test/client/tc_list_metadata_formats.rb +18 -0
  70. data/test/client/tc_list_records.rb +13 -0
  71. data/test/client/tc_list_sets.rb +19 -0
  72. data/test/client/tc_low_resolution_dates.rb +14 -0
  73. data/test/client/tc_utf8_escaping.rb +11 -0
  74. data/test/client/tc_xpath.rb +26 -0
  75. data/test/client/test_helper.rb +5 -0
  76. data/test/provider/models.rb +234 -0
  77. data/test/provider/tc_exceptions.rb +96 -0
  78. data/test/provider/tc_functional_tokens.rb +43 -0
  79. data/test/provider/tc_provider.rb +71 -0
  80. data/test/provider/tc_resumption_tokens.rb +46 -0
  81. data/test/provider/tc_simple_provider.rb +92 -0
  82. data/test/provider/test_helper.rb +36 -0
  83. data/test/test.xml +22 -0
  84. metadata +181 -0
@@ -0,0 +1,134 @@
1
+ require 'active_record'
2
+
3
+ module OAI::Provider
4
+
5
+ # ActiveRecord model class in support of the caching wrapper.
6
+ class OaiToken < ActiveRecord::Base
7
+ has_many :entries, :class_name => 'OaiEntry',
8
+ :order => "record_id", :dependent => :destroy
9
+
10
+ validates_uniqueness_of :token
11
+
12
+ # Make sanitize_sql a public method so we can make use of it.
13
+ public
14
+
15
+ def self.sanitize_sql(*arg)
16
+ super(*arg)
17
+ end
18
+
19
+ def new_record_before_save?
20
+ @new_record_before_save
21
+ end
22
+
23
+ end
24
+
25
+ # ActiveRecord model class in support of the caching wrapper.
26
+ class OaiEntry < ActiveRecord::Base
27
+ belongs_to :oai_token
28
+
29
+ validates_uniqueness_of :record_id, :scope => :oai_token
30
+ end
31
+
32
+ # = OAI::Provider::ActiveRecordCachingWrapper
33
+ #
34
+ # This class wraps an ActiveRecord model and delegates all of the record
35
+ # selection/retrieval to the AR model. It accepts options for specifying
36
+ # the update timestamp field, a timeout, and a limit. The limit option
37
+ # is used for doing pagination with resumption tokens. The timeout is
38
+ # used to expire old tokens from the cache. Default timeout is 12 hours.
39
+ #
40
+ # The difference between ActiveRecordWrapper and this class is how the
41
+ # pagination is accomplished. ActiveRecordWrapper encodes all the
42
+ # information in the token. That approach should work 99% of the time.
43
+ # If you have an extremely active respository you may want to consider
44
+ # the caching wrapper. The caching wrapper takes the entire result set
45
+ # from a request and caches it in another database table, well tables
46
+ # actually. So the result returned to the client will always be
47
+ # internally consistent.
48
+ #
49
+ class ActiveRecordCachingWrapper < ActiveRecordWrapper
50
+
51
+ attr_reader :model, :timestamp_field, :expire
52
+
53
+ def initialize(model, options={})
54
+ @expire = options.delete(:timeout) || 12.hours
55
+ super(model, options)
56
+ end
57
+
58
+ def find(selector, options={})
59
+ sweep_cache
60
+ return next_set(options[:resumption_token]) if options[:resumption_token]
61
+
62
+ conditions = sql_conditions(options)
63
+
64
+ if :all == selector
65
+ total = model.count(:id, :conditions => conditions)
66
+ if @limit && total > @limit
67
+ select_partial(
68
+ ResumptionToken.new(options.merge({:last => 0})))
69
+ else
70
+ model.find(:all, :conditions => conditions)
71
+ end
72
+ else
73
+ model.find(selector, :conditions => conditions)
74
+ end
75
+ end
76
+
77
+ protected
78
+
79
+ def next_set(token_string)
80
+ raise ResumptionTokenException.new unless @limit
81
+
82
+ token = ResumptionToken.parse(token_string)
83
+ total = model.count(:id, :conditions => token_conditions(token))
84
+
85
+ if token.last * @limit + @limit < total
86
+ select_partial(token)
87
+ else
88
+ select_partial(token).records
89
+ end
90
+ end
91
+
92
+ # select a subset of the result set, and return it with a
93
+ # resumption token to get the next subset
94
+ def select_partial(token)
95
+ if 0 == token.last
96
+ oaitoken = OaiToken.find_or_create_by_token(token.to_s)
97
+ if oaitoken.new_record_before_save?
98
+ OaiToken.connection.execute("insert into " +
99
+ "#{OaiEntry.table_name} (oai_token_id, record_id) " +
100
+ "select #{oaitoken.id}, id from #{model.table_name} where " +
101
+ "#{OaiToken.sanitize_sql(token_conditions(token))}")
102
+ end
103
+ end
104
+
105
+ oaitoken = OaiToken.find_by_token(token.to_s)
106
+ raise ResumptionTokenException.new unless oaitoken
107
+
108
+ PartialResult.new(
109
+ hydrate_records(oaitoken.entries.find(:all, :limit => @limit,
110
+ :offset => token.last * @limit)), token.next(token.last + 1)
111
+ )
112
+ end
113
+
114
+ def sweep_cache
115
+ OaiToken.destroy_all(["created_at < ?", Time.now - expire])
116
+ end
117
+
118
+ def hydrate_records(records)
119
+ model.find(records.collect {|r| r.record_id })
120
+ end
121
+
122
+ def token_conditions(token)
123
+ sql_conditions token.to_conditions_hash
124
+ end
125
+
126
+ private
127
+
128
+ def expires_at(creation)
129
+ created = Time.parse(creation.strftime("%Y-%m-%d %H:%M:%S"))
130
+ created.utc + expire
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,139 @@
1
+ require 'active_record'
2
+ module OAI::Provider
3
+ # = OAI::Provider::ActiveRecordWrapper
4
+ #
5
+ # This class wraps an ActiveRecord model and delegates all of the record
6
+ # selection/retrieval to the AR model. It accepts options for specifying
7
+ # the update timestamp field, a timeout, and a limit. The limit option
8
+ # is used for doing pagination with resumption tokens. The
9
+ # expiration timeout is ignored, since all necessary information is
10
+ # encoded in the token.
11
+ #
12
+ class ActiveRecordWrapper < Model
13
+
14
+ attr_reader :model, :timestamp_field
15
+
16
+ def initialize(model, options={})
17
+ @model = model
18
+ @timestamp_field = options.delete(:timestamp_field) || 'updated_at'
19
+ @limit = options.delete(:limit)
20
+
21
+ unless options.empty?
22
+ raise ArgumentError.new(
23
+ "Unsupported options [#{options.keys.join(', ')}]"
24
+ )
25
+ end
26
+ end
27
+
28
+ def earliest
29
+ model.find(:first,
30
+ :order => "#{timestamp_field} asc").send(timestamp_field)
31
+ end
32
+
33
+ def latest
34
+ model.find(:first,
35
+ :order => "#{timestamp_field} desc").send(timestamp_field)
36
+ end
37
+ # A model class is expected to provide a method Model.sets that
38
+ # returns all the sets the model supports. See the
39
+ # activerecord_provider tests for an example.
40
+ def sets
41
+ model.sets if model.respond_to?(:sets)
42
+ end
43
+
44
+ def find(selector, options={})
45
+ return next_set(options[:resumption_token]) if options[:resumption_token]
46
+ conditions = sql_conditions(options)
47
+ if :all == selector
48
+ total = model.count(:id, :conditions => conditions)
49
+ if @limit && total > @limit
50
+ select_partial(ResumptionToken.new(options.merge({:last => 0})))
51
+ else
52
+ model.find(:all, :conditions => conditions)
53
+ end
54
+ else
55
+ begin
56
+ model.find(selector, :conditions => conditions)
57
+ rescue ActiveRecord::RecordNotFound
58
+ raise OAI::IdException.new
59
+ end
60
+ end
61
+ end
62
+
63
+ def deleted?(record)
64
+ if record.respond_to?(:deleted_at)
65
+ return record.deleted_at
66
+ elsif record.respond_to?(:deleted)
67
+ return record.deleted
68
+ end
69
+ false
70
+ end
71
+
72
+ protected
73
+
74
+ # Request the next set in this sequence.
75
+ def next_set(token_string)
76
+ raise OAI::ResumptionTokenException.new unless @limit
77
+
78
+ token = ResumptionToken.parse(token_string)
79
+ total = model.count(:id, :conditions => token_conditions(token))
80
+
81
+ if @limit < total
82
+ select_partial(token)
83
+ else # end of result set
84
+ model.find(:all,
85
+ :conditions => token_conditions(token),
86
+ :limit => @limit, :order => "#{model.primary_key} asc")
87
+ end
88
+ end
89
+
90
+ # select a subset of the result set, and return it with a
91
+ # resumption token to get the next subset
92
+ def select_partial(token)
93
+ records = model.find(:all,
94
+ :conditions => token_conditions(token),
95
+ :limit => @limit,
96
+ :order => "#{model.primary_key} asc")
97
+ raise OAI::ResumptionTokenException.new unless records
98
+ offset = records.last.send(model.primary_key.to_sym)
99
+
100
+ PartialResult.new(records, token.next(offset))
101
+ end
102
+
103
+ # build a sql conditions statement from the content
104
+ # of a resumption token. It is very important not to
105
+ # miss any changes as records may change scope as the
106
+ # harvest is in progress. To avoid loosing any changes
107
+ # the last 'id' of the previous set is used as the
108
+ # filter to the next set.
109
+ def token_conditions(token)
110
+ last = token.last
111
+ sql = sql_conditions token.to_conditions_hash
112
+
113
+ return sql if 0 == last
114
+ # Now add last id constraint
115
+ sql[0] << " AND #{model.primary_key} > ?"
116
+ sql << last
117
+
118
+ return sql
119
+ end
120
+
121
+ # build a sql conditions statement from an OAI options hash
122
+ def sql_conditions(opts)
123
+ sql = []
124
+ sql << "#{timestamp_field} >= ?" << "#{timestamp_field} <= ?"
125
+ sql << "set = ?" if opts[:set]
126
+ esc_values = [sql.join(" AND ")]
127
+ esc_values << get_time(opts[:from]).localtime << get_time(opts[:until]).localtime #-- OAI 2.0 hack - UTC fix from record_responce
128
+ esc_values << opts[:set] if opts[:set]
129
+
130
+ return esc_values
131
+ end
132
+
133
+ def get_time(time)
134
+ time.kind_of?(Time) ? time : Time.parse(time)
135
+ end
136
+
137
+ end
138
+ end
139
+
@@ -0,0 +1,74 @@
1
+ module OAI::Provider
2
+ # = OAI::Provider::Model
3
+ #
4
+ # Model implementers should subclass OAI::Provider::Model and override
5
+ # Model#earliest, Model#latest, and Model#find. Optionally Model#sets and
6
+ # Model#deleted? can be used to support sets and record deletions. It
7
+ # is also the responsibility of the model implementer to account for
8
+ # resumption tokens if support is required. Models that don't support
9
+ # resumption tokens should raise an exception if a limit is requested
10
+ # during initialization.
11
+ #
12
+ # earliest - should return the earliest update time in the repository.
13
+ # latest - should return the most recent update time in the repository.
14
+ # sets - should return an array of sets supported by the repository.
15
+ # deleted? - individual records returned should respond true or false
16
+ # when sent the deleted? message.
17
+ # available_formats - if overridden, individual records should return an
18
+ # array of prefixes for all formats in which that record is available,
19
+ # if other than ["oai_dc"]
20
+ #
21
+ # == Resumption Tokens
22
+ #
23
+ # For examples of using resumption tokens see the
24
+ # ActiveRecordWrapper, and ActiveRecordCachingWrapper classes.
25
+ #
26
+ # There are several helper models for dealing with resumption tokens please
27
+ # see the ResumptionToken class for more details.
28
+ #
29
+
30
+ class Model
31
+ attr_reader :timestamp_field
32
+
33
+ def initialize(limit = nil, timestamp_field = 'updated_at')
34
+ @limit = limit
35
+ @timestamp_field = timestamp_field
36
+ end
37
+
38
+ # should return the earliest timestamp available from this model.
39
+ def earliest
40
+ raise NotImplementedError.new
41
+ end
42
+
43
+ # should return the latest timestamp available from this model.
44
+ def latest
45
+ raise NotImplementedError.new
46
+ end
47
+
48
+ def sets
49
+ nil
50
+ end
51
+
52
+ # find is the core method of a model, it returns records from the model
53
+ # bases on the parameters passed in.
54
+ #
55
+ # <tt>selector</tt> can be a singular id, or the symbol :all
56
+ # <tt>options</tt> is a hash of options to be used to constrain the query.
57
+ #
58
+ # Valid options:
59
+ # * :from => earliest timestamp to be included in the results
60
+ # * :until => latest timestamp to be included in the results
61
+ # * :set => the set from which to retrieve the results
62
+ # * :metadata_prefix => type of metadata requested (this may be useful if
63
+ # not all records are available in all formats)
64
+ def find(selector, options={})
65
+ raise NotImplementedError.new
66
+ end
67
+
68
+ def deleted?
69
+ false
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,18 @@
1
+ module OAI::Provider
2
+ # = OAI::Provider::PartialResult
3
+ #
4
+ # PartialResult is used for returning a set/page of results from a model
5
+ # that supports resumption tokens. It should contain and array of
6
+ # records, and a resumption token for getting the next set/page.
7
+ #
8
+ class PartialResult
9
+ attr_reader :records, :token
10
+
11
+ def initialize(records, token = nil)
12
+ @records = records
13
+ @token = token
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,16 @@
1
+ module OAI::Provider::Response
2
+ class Error < Base
3
+
4
+ def initialize(provider, error)
5
+ super(provider)
6
+ @error = error
7
+ end
8
+
9
+ def to_xml
10
+ response do |r|
11
+ r.error @error.to_s, :code => @error.code
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module OAI::Provider::Response
2
+
3
+ class GetRecord < RecordResponse
4
+ required_parameters :identifier, :metadata_prefix
5
+
6
+ def to_xml
7
+ id = extract_identifier(options.delete(:identifier))
8
+ unless record = provider.model.find(id, options)
9
+ raise OAI::IdException.new
10
+ end
11
+
12
+ response do |r|
13
+ r.GetRecord do
14
+ r.record do
15
+ header_for record
16
+ data_for record unless deleted?(record)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+
@@ -0,0 +1,25 @@
1
+ module OAI::Provider::Response
2
+
3
+ class Identify < Base
4
+
5
+ def to_xml
6
+ response do |r|
7
+ r.Identify do
8
+ r.repositoryName provider.name
9
+ r.baseURL provider.url
10
+ r.protocolVersion 2.0
11
+ if provider.email and provider.email.respond_to?(:each)
12
+ provider.email.each { |address| r.adminEmail address }
13
+ else
14
+ r.adminEmail provider.email.to_s
15
+ end
16
+ r.earliestDatestamp Time.parse(provider.model.earliest.to_s).utc.xmlschema
17
+ r.deletedRecord provider.delete_support.to_s
18
+ r.granularity provider.granularity
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,35 @@
1
+ module OAI::Provider::Response
2
+
3
+ class ListIdentifiers < RecordResponse
4
+ required_parameters :metadata_prefix
5
+
6
+ def to_xml
7
+ result = provider.model.find(:all, options)
8
+
9
+ # result may be an array of records, or a partial result
10
+ records = result.respond_to?(:records) ? result.records : result
11
+
12
+ raise OAI::NoMatchException.new if records.nil? or records.empty?
13
+ format = requested_format # not call this in each iteration
14
+ records.reject! do |r|
15
+ !record_supports(r, format)
16
+ end
17
+
18
+ response do |r|
19
+ r.ListIdentifiers do
20
+ records.each do |rec|
21
+ header_for rec
22
+ end
23
+
24
+ # append resumption token for getting next group of records
25
+ if result.respond_to?(:token)
26
+ r.target! << result.token.to_xml
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,34 @@
1
+ module OAI::Provider::Response
2
+ class ListMetadataFormats < RecordResponse
3
+ valid_parameters :identifier
4
+
5
+ def to_xml
6
+ # Get a list of all the formats the provider understands.
7
+ formats = provider.formats.values
8
+
9
+ # if it's a doc-specific request
10
+ if options.include?(:identifier)
11
+ id = extract_identifier(options[:identifier])
12
+ unless record = provider.model.find(id, options)
13
+ raise OAI::IdException.new
14
+ end
15
+
16
+ # Remove any format that this particular record can't be provided in.
17
+ formats.reject! { |f| !record_supports(record, f.prefix) }
18
+ end
19
+ response do |r|
20
+ r.ListMetadataFormats do
21
+ formats.each do |format|
22
+ r.metadataFormat do
23
+ r.metadataPrefix format.prefix
24
+ r.schema format.schema
25
+ r.metadataNamespace format.namespace
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module OAI::Provider::Response
2
+
3
+ class ListRecords < RecordResponse
4
+ required_parameters :metadata_prefix
5
+
6
+ def to_xml
7
+ result = provider.model.find(:all, options)
8
+ # result may be an array of records, or a partial result
9
+ records = result.respond_to?(:records) ? result.records : result
10
+
11
+ raise OAI::NoMatchException.new if records.nil? or records.empty?
12
+
13
+ response do |r|
14
+ r.ListRecords do
15
+ records.each do |rec|
16
+ r.record do
17
+ header_for rec
18
+ data_for rec unless deleted?(rec)
19
+ end
20
+ end
21
+
22
+ # append resumption token for getting next group of records
23
+ if result.respond_to?(:token)
24
+ r.target! << result.token.to_xml
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1,23 @@
1
+ module OAI::Provider::Response
2
+
3
+ class ListSets < Base
4
+
5
+ def to_xml
6
+ raise OAI::SetException.new unless provider.model.sets
7
+
8
+ response do |r|
9
+ r.ListSets do
10
+ provider.model.sets.each do |set|
11
+ r.set do
12
+ r.setSpec set.spec
13
+ r.setName set.name
14
+ r.setDescription(set.description) if set.respond_to?(:description)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,70 @@
1
+ module OAI::Provider::Response
2
+ class RecordResponse < Base
3
+ def self.inherited(klass)
4
+ klass.valid_parameters :metadata_prefix, :from, :until, :set
5
+ klass.default_parameters :from => Proc.new {|x| Time.parse(x.provider.model.earliest.to_s) },
6
+ :until => Proc.new {|x| Time.parse(x.provider.model.latest.to_s) }
7
+ end
8
+
9
+ # emit record header
10
+ def header_for(record)
11
+ param = Hash.new
12
+ param[:status] = 'deleted' if deleted?(record)
13
+ @builder.header param do
14
+ @builder.identifier identifier_for(record)
15
+ @builder.datestamp timestamp_for(record)
16
+ sets_for(record).each do |set|
17
+ @builder.setSpec set.spec
18
+ end
19
+ end
20
+ end
21
+ # metadata - core routine for delivering metadata records
22
+ #
23
+ def data_for(record)
24
+ @builder.metadata do
25
+ @builder.target! << provider.format(requested_format).encode(provider.model, record)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def identifier_for(record)
32
+ "#{provider.prefix}/#{record.id}"
33
+ end
34
+
35
+ def timestamp_for(record)
36
+ record.send(provider.model.timestamp_field).utc.xmlschema
37
+ end
38
+
39
+ def sets_for(record)
40
+ return [] unless record.respond_to?(:sets) and record.sets
41
+ record.sets.respond_to?(:each) ? record.sets : [record.sets]
42
+ end
43
+
44
+ def requested_format
45
+ format =
46
+ if options[:metadata_prefix]
47
+ options[:metadata_prefix]
48
+ elsif options[:resumption_token]
49
+ OAI::Provider::ResumptionToken.extract_format(options[:resumption_token])
50
+ end
51
+ raise OAI::FormatException.new unless provider.format_supported?(format)
52
+
53
+ format
54
+ end
55
+
56
+ def deleted?(record)
57
+ return record.deleted? if record.respond_to?(:deleted?)
58
+ return record.deleted if record.respond_to?(:deleted)
59
+ return record.deleted_at if record.respond_to?(:deleted_at)
60
+ false
61
+ end
62
+
63
+ def record_supports(record, prefix)
64
+ prefix == 'oai_dc' or
65
+ record.respond_to?("to_#{prefix}") or
66
+ record.respond_to?("map_#{prefix}")
67
+ end
68
+
69
+ end
70
+ end