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,119 @@
1
+ require 'singleton'
2
+
3
+ module OAI::Provider::Metadata
4
+ # == Metadata Base Class
5
+ #
6
+ # MetadataFormat is the base class from which all other format classes
7
+ # should inherit. Format classes provide mapping of record fields into XML.
8
+ #
9
+ # * prefix - contains the metadata_prefix used to select the format
10
+ # * schema - location of the xml schema
11
+ # * namespace - location of the namespace document
12
+ # * element_namespace - the namespace portion of the XML elements
13
+ # * fields - list of fields in this metadata format
14
+ #
15
+ # See OAI::Metadata::DublinCore for an example
16
+ #
17
+ class Format
18
+ include Singleton
19
+
20
+ attr_accessor :prefix, :schema, :namespace, :element_namespace, :fields
21
+
22
+ # Provided a model, and a record belonging to that model this method
23
+ # will return an xml represention of the record. This is the method
24
+ # that should be extended if you need to create more complex xml
25
+ # representations.
26
+ def encode(model, record)
27
+ if record.respond_to?("to_#{prefix}")
28
+ record.send("to_#{prefix}")
29
+ else
30
+ xml = Builder::XmlMarkup.new
31
+ map = model.respond_to?("map_#{prefix}") ? model.send("map_#{prefix}") : {}
32
+ xml.tag!("#{prefix}:#{element_namespace}", header_specification) do
33
+ fields.each do |field|
34
+ values = value_for(field, record, map)
35
+ if values.respond_to?(:each)
36
+ values.each do |value|
37
+ xml.tag! "#{element_namespace}:#{field}", value
38
+ end
39
+ else
40
+ xml.tag! "#{element_namespace}:#{field}", values
41
+ end
42
+ end
43
+ end
44
+ xml.target!
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # We try a bunch of different methods to get the data from the model.
51
+ #
52
+ # 1. Check if the model defines a field mapping for the field of
53
+ # interest.
54
+ # 2. Try calling the pluralized name method on the model.
55
+ # 3. Try calling the singular name method on the model
56
+ def value_for(field, record, map)
57
+ method = map[field] ? map[field].to_s : field.to_s
58
+
59
+ if record.respond_to?(pluralize(method))
60
+ record.send pluralize(method)
61
+ elsif method != 'type' and record.respond_to?(method)
62
+ # TODO: this calls type, which is deprecated, should be
63
+ record.send method
64
+ else
65
+ []
66
+ end
67
+ end
68
+
69
+ # Subclasses must override
70
+ def header_specification
71
+ raise NotImplementedError.new
72
+ end
73
+
74
+ # Shamelessly lifted form ActiveSupport. Thanks Rails community!
75
+ def pluralize(word)
76
+ # Use ActiveSupports pluralization if it's available.
77
+ return word.pluralize if word.respond_to?(:pluralize)
78
+
79
+ # Otherwise use our own simple pluralization rules.
80
+ result = word.to_s.dup
81
+
82
+ # Uncountable words
83
+ return result if %w(equipment information rice money species series fish sheep).include?(result)
84
+
85
+ # Irregular words
86
+ { 'person' => 'people', 'man' => 'men', 'child' => 'children', 'sex' => 'sexes',
87
+ 'move' => 'moves', 'cow' => 'kine' }.each { |k,v| return v if word == k }
88
+
89
+ rules.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
90
+ result
91
+ end
92
+
93
+ def rules
94
+ [
95
+ [/$/, 's'],
96
+ [/s$/i, 's'],
97
+ [/(ax|test)is$/i, '\1es'],
98
+ [/(octop|vir)us$/i, '\1i'],
99
+ [/(alias|status)$/i, '\1es'],
100
+ [/(bu)s$/i, '\1ses'],
101
+ [/(buffal|tomat)o$/i, '\1oes'],
102
+ [/([ti])um$/i, '\1a'],
103
+ [/sis$/i, 'ses'],
104
+ [/(?:([^f])fe|([lr])f)$/i, '\1\2ves'],
105
+ [/(hive)$/i, '\1s'],
106
+ [/([^aeiouy]|qu)y$/i, '\1ies'],
107
+ [/(x|ch|ss|sh)$/i, '\1es'],
108
+ [/(matr|vert|ind)(?:ix|ex)$/i, '\1ices'],
109
+ [/([m|l])ouse$/i, '\1ice'],
110
+ [/^(ox)$/i, '\1en'],
111
+ [/(quiz)$/i, '\1zes']
112
+ ]
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ Dir.glob(File.dirname(__FILE__) + '/metadata_format/*.rb').each {|lib| require lib}
@@ -0,0 +1,129 @@
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, -> { order("record_id ASC") },
8
+ :class_name => 'OaiEntry', :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
+ # This class wraps an ActiveRecord model and delegates all of the record
33
+ # selection/retrieval to the AR model. It accepts options for specifying
34
+ # the update timestamp field, a timeout, and a limit. The limit option
35
+ # is used for doing pagination with resumption tokens. The timeout is
36
+ # used to expire old tokens from the cache. Default timeout is 12 hours.
37
+ #
38
+ # The difference between ActiveRecordWrapper and this class is how the
39
+ # pagination is accomplished. ActiveRecordWrapper encodes all the
40
+ # information in the token. That approach should work 99% of the time.
41
+ # If you have an extremely active respository you may want to consider
42
+ # the caching wrapper. The caching wrapper takes the entire result set
43
+ # from a request and caches it in another database table, well tables
44
+ # actually. So the result returned to the client will always be
45
+ # internally consistent.
46
+ #
47
+ class ActiveRecordCachingWrapper < ActiveRecordWrapper
48
+
49
+ attr_reader :model, :timestamp_field, :expire
50
+
51
+ def initialize(model, options={})
52
+ @expire = options.delete(:timeout) || 12.hours
53
+ super(model, options)
54
+ end
55
+
56
+ def find(selector, options={})
57
+ sweep_cache
58
+ return next_set(options[:resumption_token]) if options[:resumption_token]
59
+
60
+ conditions = sql_conditions(options)
61
+
62
+ if :all == selector
63
+ total = model.where(conditions).count
64
+ if @limit && total > @limit
65
+ select_partial(
66
+ ResumptionToken.new(options.merge({:last => 0})))
67
+ else
68
+ model.where(conditions)
69
+ end
70
+ else
71
+ model.where(conditions).find(selector)
72
+ end
73
+ end
74
+
75
+ protected
76
+
77
+ def next_set(token_string)
78
+ raise ResumptionTokenException.new unless @limit
79
+
80
+ token = ResumptionToken.parse(token_string)
81
+ select_partial(token)
82
+ end
83
+
84
+ # select a subset of the result set, and return it with a
85
+ # resumption token to get the next subset
86
+ def select_partial(token)
87
+ oaitoken = OaiToken.find_by(token: token.to_s)
88
+
89
+ if 0 == token.last && oaitoken.nil?
90
+ oaitoken = OaiToken.create!(token: token.to_s)
91
+ OaiToken.connection.execute("insert into " +
92
+ "#{OaiEntry.table_name} (oai_token_id, record_id) " +
93
+ "select #{oaitoken.id}, id from #{model.table_name} where " +
94
+ "#{OaiToken.sanitize_sql(token_conditions(token))}")
95
+ end
96
+
97
+ raise ResumptionTokenException.new unless oaitoken
98
+
99
+ total = model.where(token_conditions(token)).count
100
+ # token offset should be nil if this is the last set
101
+ offset = (token.last * @limit + @limit >= total) ? nil : token.last + 1
102
+ PartialResult.new(
103
+ hydrate_records(
104
+ oaitoken.entries.limit(@limit).offset(token.last * @limit)),
105
+ token.next(offset)
106
+ )
107
+ end
108
+
109
+ def sweep_cache
110
+ OaiToken.where(["created_at < ?", Time.now - expire]).destroy_all
111
+ end
112
+
113
+ def hydrate_records(records)
114
+ model.find(records.collect {|r| r.record_id })
115
+ end
116
+
117
+ def token_conditions(token)
118
+ sql_conditions token.to_conditions_hash
119
+ end
120
+
121
+ private
122
+
123
+ def expires_at(creation)
124
+ created = Time.parse(creation.strftime("%Y-%m-%d %H:%M:%S"))
125
+ created.utc + expire
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,217 @@
1
+ require 'active_record'
2
+
3
+ module OAI::Provider
4
+ # This class wraps an ActiveRecord model and delegates all of the record
5
+ # selection/retrieval to the AR model. It accepts options for specifying
6
+ # the update timestamp field, a timeout, and a limit. The limit option
7
+ # is used for doing pagination with resumption tokens. The
8
+ # expiration timeout is ignored, since all necessary information is
9
+ # encoded in the token.
10
+ #
11
+ class ActiveRecordWrapper < Model
12
+
13
+ attr_reader :model, :timestamp_field, :identifier_field
14
+
15
+ # If custom 'timestamp_field' is used, be aware this will be an ActiveRecord
16
+ # attribute that we will limit on, so perhaps should be indexe appropriately.
17
+ #
18
+ # If custom `identifier_field` is used, be aware this will be an ActiveRecord
19
+ # attribute that we will sort on, and use in WHERE clauses with `=` as well as
20
+ # greater than/less than, so should be indexed appropriately.
21
+ def initialize(model, options={})
22
+ @model = model
23
+ @timestamp_field = options.delete(:timestamp_field) || 'updated_at'
24
+ @identifier_field = options.delete(:identifier_field) || model.primary_key || "id"
25
+ @limit = options.delete(:limit) || 100
26
+
27
+ unless options.empty?
28
+ raise ArgumentError.new(
29
+ "Unsupported options [#{options.keys.join(', ')}]"
30
+ )
31
+ end
32
+ end
33
+
34
+ def earliest
35
+ earliest_obj = model.order("#{model.base_class.table_name}.#{timestamp_field} asc").first
36
+ earliest_obj.nil? ? Time.at(0) : earliest_obj.send(timestamp_field)
37
+ end
38
+
39
+ def latest
40
+ latest_obj = model.order("#{model.base_class.table_name}.#{timestamp_field} desc").first
41
+ latest_obj.nil? ? Time.now : latest_obj.send(timestamp_field)
42
+ end
43
+ # A model class is expected to provide a method Model.sets that
44
+ # returns all the sets the model supports. See the
45
+ # activerecord_provider tests for an example.
46
+ def sets
47
+ model.respond_to?(:sets) ? model.sets : []
48
+ end
49
+
50
+ def find(selector, options={})
51
+ find_scope = find_scope(options)
52
+ return next_set(find_scope,
53
+ options[:resumption_token]) if options[:resumption_token]
54
+ conditions = sql_conditions(options)
55
+ if :all == selector
56
+ total = find_scope.where(conditions).count
57
+ if @limit && total > @limit
58
+ select_partial(find_scope,
59
+ ResumptionToken.new(options.merge({:last => 0})))
60
+ else
61
+ find_scope.where(conditions)
62
+ end
63
+ else
64
+ find_scope.where(conditions).where(identifier_field => selector).first
65
+ end
66
+ end
67
+
68
+ def deleted?(record)
69
+ if record.respond_to?(:deleted_at)
70
+ return record.deleted_at
71
+ elsif record.respond_to?(:deleted)
72
+ return record.deleted
73
+ end
74
+ false
75
+ end
76
+
77
+ def respond_to?(m, *args)
78
+ if m =~ /^map_/
79
+ model.respond_to?(m, *args)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def method_missing(m, *args, &block)
86
+ if m =~ /^map_/
87
+ model.send(m, *args, &block)
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ def find_scope(options)
96
+ return model unless options.key?(:set)
97
+
98
+ # Find the set or return an empty scope
99
+ set = find_set_by_spec(options[:set])
100
+ return model.limit(0) if set.nil?
101
+
102
+ # If the set has a backward relationship, we'll use it
103
+ if set.class.respond_to?(:reflect_on_all_associations)
104
+ set.class.reflect_on_all_associations.each do |assoc|
105
+ return set.send(assoc.name) if assoc.klass == model
106
+ end
107
+ end
108
+
109
+ # Search the attributes for 'set'
110
+ if model.column_names.include?('set')
111
+ # Scope using the set attribute as the spec
112
+ model.where(set: options[:set])
113
+ else
114
+ # Default to empty set, as we've tried everything else
115
+ model.none
116
+ end
117
+ end
118
+
119
+ def find_set_by_spec(spec)
120
+ if sets.class == ActiveRecord::Relation
121
+ sets.find_by_spec(spec)
122
+ else
123
+ sets.detect {|set| set.spec == spec}
124
+ end
125
+ end
126
+
127
+ # Request the next set in this sequence.
128
+ def next_set(find_scope, token_string)
129
+ raise OAI::ResumptionTokenException.new unless @limit
130
+
131
+ token = ResumptionToken.parse(token_string)
132
+ select_partial(find_scope, token)
133
+ end
134
+
135
+ # select a subset of the result set, and return it with a
136
+ # resumption token to get the next subset
137
+ def select_partial(find_scope, token)
138
+ records = find_scope.where(token_conditions(token))
139
+ .limit(@limit)
140
+ .order("#{model.base_class.table_name}.#{identifier_field} asc")
141
+ raise OAI::ResumptionTokenException.new unless records
142
+
143
+ total = find_scope.where(token_conditions(token)).count
144
+ # token offset should be nil if this is the last set
145
+ offset = (@limit >= total) ? nil : records.last.send(identifier_field)
146
+ PartialResult.new(records, token.next(offset))
147
+ end
148
+
149
+ # build a sql conditions statement from the content
150
+ # of a resumption token. It is very important not to
151
+ # miss any changes as records may change scope as the
152
+ # harvest is in progress. To avoid loosing any changes
153
+ # the last 'id' of the previous set is used as the
154
+ # filter to the next set.
155
+ def token_conditions(token)
156
+ last_id = token.last_str
157
+ sql = sql_conditions token.to_conditions_hash
158
+
159
+ return sql if "0" == last_id
160
+ # Now add last id constraint
161
+ sql.first << " AND #{model.base_class.table_name}.#{identifier_field} > :id"
162
+ sql.last[:id] = last_id
163
+
164
+ return sql
165
+ end
166
+
167
+ # build a sql conditions statement from an OAI options hash
168
+ def sql_conditions(opts)
169
+ sql = []
170
+ esc_values = {}
171
+ if opts.has_key?(:from)
172
+ sql << "#{model.base_class.table_name}.#{timestamp_field} >= :from"
173
+ esc_values[:from] = parse_to_local(opts[:from])
174
+ end
175
+ if opts.has_key?(:until)
176
+ # Handle databases which store fractions of a second by rounding up
177
+ sql << "#{model.base_class.table_name}.#{timestamp_field} < :until"
178
+ esc_values[:until] = parse_to_local(opts[:until]) { |t| t + 1 }
179
+ end
180
+
181
+ return [sql.join(" AND "), esc_values]
182
+ end
183
+
184
+ private
185
+
186
+ def parse_to_local(time)
187
+ if time.respond_to?(:strftime)
188
+ time_obj = time
189
+ else
190
+ begin
191
+ if time[-1] == "Z"
192
+ time_obj = Time.strptime(time, "%Y-%m-%dT%H:%M:%S%Z")
193
+ else
194
+ time_obj = Date.strptime(time, "%Y-%m-%d")
195
+ end
196
+ rescue
197
+ raise OAI::ArgumentException.new, "unparsable date: '#{time}'"
198
+ end
199
+ end
200
+
201
+ time_obj = yield(time_obj) if block_given?
202
+
203
+ if time_obj.kind_of?(Date)
204
+ time_obj.strftime("%Y-%m-%d")
205
+ else
206
+ # Convert to same as DB - :local => :getlocal, :utc => :getutc
207
+ if ActiveRecord::VERSION::MAJOR >= 7
208
+ tzconv = "get#{ActiveRecord.default_timezone.to_s}".to_sym
209
+ else
210
+ tzconv = "get#{model.default_timezone.to_s}".to_sym
211
+ end
212
+ time_obj.send(tzconv).strftime("%Y-%m-%d %H:%M:%S")
213
+ end
214
+ end
215
+
216
+ end
217
+ end
@@ -0,0 +1,80 @@
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
+ # about - if overridden, should return a String or Array of XML Strings to
21
+ # insert into the OAI Record <about> chunks.
22
+ #
23
+ # == Resumption Tokens
24
+ #
25
+ # For examples of using resumption tokens see the
26
+ # ActiveRecordWrapper, and ActiveRecordCachingWrapper classes.
27
+ #
28
+ # There are several helper models for dealing with resumption tokens please
29
+ # see the ResumptionToken class for more details.
30
+ #
31
+ class Model
32
+ attr_reader :timestamp_field, :identifier_field, :limit
33
+
34
+ def initialize(limit = nil, timestamp_field = 'updated_at', identifier_field = 'id')
35
+ @limit = limit
36
+ @identifier_field = identifier_field
37
+ @timestamp_field = timestamp_field
38
+ end
39
+
40
+ # should return the earliest timestamp available from this model.
41
+ def earliest
42
+ raise NotImplementedError.new
43
+ end
44
+
45
+ # should return the latest timestamp available from this model.
46
+ def latest
47
+ raise NotImplementedError.new
48
+ end
49
+
50
+ def sets
51
+ nil
52
+ end
53
+
54
+ # find is the core method of a model, it returns records from the model
55
+ # bases on the parameters passed in.
56
+ #
57
+ # <tt>selector</tt> can be a singular id, or the symbol :all
58
+ # <tt>options</tt> is a hash of options to be used to constrain the query.
59
+ #
60
+ # Valid options:
61
+ # * :from => earliest timestamp to be included in the results
62
+ # * :until => latest timestamp to be included in the results
63
+ # * :set => the set from which to retrieve the results
64
+ # * :metadata_prefix => type of metadata requested (this may be useful if
65
+ # not all records are available in all formats)
66
+ def find(selector, options={})
67
+ raise NotImplementedError.new
68
+ end
69
+
70
+ def deleted?
71
+ false
72
+ end
73
+
74
+ # can return a String or Array of XML Strings add as OAI Record <about> chunks.
75
+ def about record
76
+ nil
77
+ end
78
+ end
79
+
80
+ 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,27 @@
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
+ about_for record unless deleted?(record)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+
@@ -0,0 +1,37 @@
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
+ r.description do
20
+ r.tag! 'oai-identifier', 'xmlns' => 'http://www.openarchives.org/OAI/2.0/oai-identifier', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai-identifier http://www.openarchives.org/OAI/2.0/oai-identifier.xsd' do
21
+ r.scheme 'oai'
22
+ r.repositoryIdentifier provider.prefix.gsub(/oai:/, '')
23
+ r.delimiter ':'
24
+ r.sampleIdentifier "#{provider.prefix}:#{provider.identifier}"
25
+ end
26
+ end
27
+ if provider.description
28
+ r.target! << provider.description
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,30 @@
1
+ module OAI::Provider::Response
2
+
3
+ class ListIdentifiers < RecordResponse
4
+
5
+ def to_xml
6
+ result = provider.model.find(:all, options)
7
+
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.ListIdentifiers do
15
+ records.each do |rec|
16
+ header_for rec
17
+ end
18
+
19
+ # append resumption token for getting next group of records
20
+ if result.respond_to?(:token)
21
+ r.target! << result.token.to_xml
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end