oai 0.0.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +128 -0
- data/Rakefile +64 -0
- data/bin/oai +66 -0
- data/examples/models/file_model.rb +63 -0
- data/examples/providers/dublin_core.rb +474 -0
- data/lib/oai/{get_record.rb → client/get_record.rb} +5 -1
- data/lib/oai/client/header.rb +18 -0
- data/lib/oai/{identify.rb → client/identify.rb} +2 -0
- data/lib/oai/{list_identifiers.rb → client/list_identifiers.rb} +1 -0
- data/lib/oai/{list_records.rb → client/list_records.rb} +6 -5
- data/lib/oai/{list_sets.rb → client/list_sets.rb} +7 -6
- data/lib/oai/client/record.rb +27 -0
- data/lib/oai/client/response.rb +53 -0
- data/lib/oai/client/resumable.rb +42 -0
- data/lib/oai/client.rb +313 -108
- data/lib/oai/constants.rb +34 -0
- data/lib/oai/exception.rb +80 -1
- data/lib/oai/harvester/config.rb +41 -0
- data/lib/oai/harvester/harvest.rb +162 -0
- data/lib/oai/harvester/logging.rb +68 -0
- data/lib/oai/harvester/mailer.rb +17 -0
- data/lib/oai/harvester/shell.rb +337 -0
- data/lib/oai/harvester.rb +43 -0
- data/lib/oai/provider/metadata_format/oai_dc.rb +28 -0
- data/lib/oai/provider/metadata_format.rb +119 -0
- data/lib/oai/provider/model/activerecord_caching_wrapper.rb +129 -0
- data/lib/oai/provider/model/activerecord_wrapper.rb +217 -0
- data/lib/oai/provider/model.rb +80 -0
- data/lib/oai/provider/partial_result.rb +18 -0
- data/lib/oai/provider/response/error.rb +16 -0
- data/lib/oai/provider/response/get_record.rb +27 -0
- data/lib/oai/provider/response/identify.rb +37 -0
- data/lib/oai/provider/response/list_identifiers.rb +30 -0
- data/lib/oai/provider/response/list_metadata_formats.rb +39 -0
- data/lib/oai/provider/response/list_records.rb +47 -0
- data/lib/oai/provider/response/list_sets.rb +23 -0
- data/lib/oai/provider/response/record_response.rb +103 -0
- data/lib/oai/provider/response.rb +133 -0
- data/lib/oai/provider/resumption_token.rb +155 -0
- data/lib/oai/provider.rb +487 -0
- data/lib/oai/set.rb +14 -5
- data/lib/oai/xpath.rb +59 -5
- data/lib/oai.rb +7 -13
- data/lib/test.rb +25 -0
- data/test/activerecord_provider/config/connection.rb +30 -0
- data/test/activerecord_provider/database/0001_oaipmh_tables.rb +70 -0
- data/test/activerecord_provider/fixtures/dc.yml +1501 -0
- data/test/activerecord_provider/helpers/providers.rb +68 -0
- data/test/activerecord_provider/helpers/set_provider.rb +24 -0
- data/test/activerecord_provider/helpers/transactional_test_case.rb +35 -0
- data/test/activerecord_provider/models/dc_field.rb +15 -0
- data/test/activerecord_provider/models/dc_lang.rb +3 -0
- data/test/activerecord_provider/models/dc_set.rb +7 -0
- data/test/activerecord_provider/models/exclusive_set_dc_field.rb +17 -0
- data/test/activerecord_provider/models/oai_token.rb +3 -0
- data/test/activerecord_provider/tc_activerecord_wrapper.rb +63 -0
- data/test/activerecord_provider/tc_ar_provider.rb +199 -0
- data/test/activerecord_provider/tc_ar_sets_provider.rb +159 -0
- data/test/activerecord_provider/tc_caching_paging_provider.rb +50 -0
- data/test/activerecord_provider/tc_simple_paging_provider.rb +71 -0
- data/test/activerecord_provider/test_helper_ar_provider.rb +7 -0
- data/test/client/helpers/provider.rb +65 -0
- data/test/client/helpers/test_wrapper.rb +6 -0
- data/test/client/tc_exception.rb +36 -0
- data/test/client/tc_get_record.rb +39 -0
- data/test/client/tc_http_client.rb +129 -0
- data/test/client/tc_identify.rb +13 -0
- data/test/client/tc_libxml.rb +61 -0
- data/test/{tc_list_identifiers.rb → client/tc_list_identifiers.rb} +28 -9
- data/test/{tc_list_metadata_formats.rb → client/tc_list_metadata_formats.rb} +4 -1
- data/test/client/tc_list_records.rb +26 -0
- data/test/client/tc_list_sets.rb +34 -0
- data/test/client/tc_low_resolution_dates.rb +14 -0
- data/test/client/tc_utf8_escaping.rb +19 -0
- data/test/client/tc_xpath.rb +26 -0
- data/test/client/test_helper_client.rb +5 -0
- data/test/harvester/tc_harvest.rb +42 -0
- data/test/harvester/test_helper_harvester.rb +6 -0
- data/test/provider/models.rb +246 -0
- data/test/provider/tc_exceptions.rb +81 -0
- data/test/provider/tc_functional_tokens.rb +53 -0
- data/test/provider/tc_instance_provider.rb +41 -0
- data/test/provider/tc_provider.rb +136 -0
- data/test/provider/tc_resumption_tokens.rb +52 -0
- data/test/provider/tc_simple_provider.rb +138 -0
- data/test/provider/test_helper_provider.rb +62 -0
- data/test/test.xml +22 -0
- metadata +238 -56
- data/lib/oai/header.rb +0 -12
- data/lib/oai/record.rb +0 -11
- data/lib/oai/response.rb +0 -20
- data/test/tc_get_record.rb +0 -27
- data/test/tc_identify.rb +0 -8
- data/test/tc_list_records.rb +0 -9
- data/test/tc_list_sets.rb +0 -17
- data/test.rb +0 -12
- /data/lib/oai/{list_metadata_formats.rb → client/list_metadata_formats.rb} +0 -0
- /data/lib/oai/{metadata_format.rb → client/metadata_format.rb} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
def record_supports(record, prefix)
|
|
33
|
+
prefix == 'oai_dc' or
|
|
34
|
+
record.respond_to?("to_#{prefix}") or
|
|
35
|
+
record.respond_to?("map_#{prefix}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module OAI::Provider::Response
|
|
2
|
+
|
|
3
|
+
class ListRecords < RecordResponse
|
|
4
|
+
required_parameters :metadata_prefix
|
|
5
|
+
|
|
6
|
+
def valid?
|
|
7
|
+
super && matching_granularity?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def matching_granularity?
|
|
11
|
+
if options[:from].nil? == false && options[:until].nil? == false && options[:from].class.name != options[:until].class.name
|
|
12
|
+
raise OAI::ArgumentException.new, "The 'from' and 'until' options specified must have the same granularity"
|
|
13
|
+
else
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_xml
|
|
19
|
+
result = provider.model.find(:all, options)
|
|
20
|
+
# result may be an array of records, or a partial result
|
|
21
|
+
records = result.respond_to?(:records) ? result.records : result
|
|
22
|
+
|
|
23
|
+
raise OAI::NoMatchException.new if records.nil? or records.empty?
|
|
24
|
+
|
|
25
|
+
response do |r|
|
|
26
|
+
r.ListRecords do
|
|
27
|
+
records.each do |rec|
|
|
28
|
+
r.record do
|
|
29
|
+
header_for rec
|
|
30
|
+
data_for rec unless deleted?(rec)
|
|
31
|
+
about_for rec unless deleted?(rec)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# append resumption token for getting next group of records
|
|
36
|
+
if result.respond_to?(:token)
|
|
37
|
+
r.target! << result.token.to_xml
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
@@ -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,103 @@
|
|
|
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 :metadata_prefix => "oai_dc",
|
|
6
|
+
:from => method(:default_from).to_proc,
|
|
7
|
+
:until => method(:default_until).to_proc
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.default_from(response)
|
|
11
|
+
value = Time.parse(response.provider.model.earliest.to_s).utc
|
|
12
|
+
if response.options[:until]
|
|
13
|
+
u = parse_date(response.options[:until])
|
|
14
|
+
value = value.to_date if u.is_a? Date
|
|
15
|
+
end
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.default_until(response)
|
|
20
|
+
value = Time.parse(response.provider.model.latest.to_s).utc
|
|
21
|
+
if response.options[:from]
|
|
22
|
+
f = parse_date(response.options[:from])
|
|
23
|
+
value = value.to_date if f.is_a? Date
|
|
24
|
+
end
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# emit record header
|
|
29
|
+
def header_for(record)
|
|
30
|
+
param = Hash.new
|
|
31
|
+
param[:status] = 'deleted' if deleted?(record)
|
|
32
|
+
@builder.header param do
|
|
33
|
+
@builder.identifier identifier_for(record)
|
|
34
|
+
@builder.datestamp timestamp_for(record)
|
|
35
|
+
sets_for(record).each do |set|
|
|
36
|
+
@builder.setSpec set.spec
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
# metadata - core routine for delivering metadata records
|
|
41
|
+
#
|
|
42
|
+
def data_for(record)
|
|
43
|
+
@builder.metadata do
|
|
44
|
+
@builder.target! << provider.format(requested_format).encode(provider.model, record)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# about - core routine for delivering about records
|
|
49
|
+
#
|
|
50
|
+
def about_for(record)
|
|
51
|
+
return unless provider.model.respond_to? :about
|
|
52
|
+
|
|
53
|
+
about = provider.model.about(record)
|
|
54
|
+
return if about.nil?
|
|
55
|
+
|
|
56
|
+
unless about.is_a? Array
|
|
57
|
+
about = [about]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
about.each do |a|
|
|
61
|
+
@builder.about do
|
|
62
|
+
@builder.target! << a
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Namespace syntax suggested in http://www.openarchives.org/OAI/2.0/guidelines-oai-identifier.htm
|
|
70
|
+
def identifier_for(record)
|
|
71
|
+
"#{provider.prefix}:#{record.send( provider.model.identifier_field )}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def timestamp_for(record)
|
|
75
|
+
record.send(provider.model.timestamp_field).utc.xmlschema
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sets_for(record)
|
|
79
|
+
return [] unless record.respond_to?(:sets) and record.sets
|
|
80
|
+
record.sets.respond_to?(:each) ? record.sets : [record.sets]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def requested_format
|
|
84
|
+
format =
|
|
85
|
+
if options[:metadata_prefix]
|
|
86
|
+
options[:metadata_prefix]
|
|
87
|
+
elsif options[:resumption_token]
|
|
88
|
+
OAI::Provider::ResumptionToken.extract_format(options[:resumption_token])
|
|
89
|
+
end
|
|
90
|
+
raise OAI::FormatException.new unless provider.format_supported?(format)
|
|
91
|
+
|
|
92
|
+
format
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def deleted?(record)
|
|
96
|
+
return record.deleted? if record.respond_to?(:deleted?)
|
|
97
|
+
return record.deleted if record.respond_to?(:deleted)
|
|
98
|
+
return record.deleted_at if record.respond_to?(:deleted_at)
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require 'builder' unless defined?(Builder)
|
|
2
|
+
module OAI
|
|
3
|
+
module Provider
|
|
4
|
+
module Response
|
|
5
|
+
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :provider, :options
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :valid_options, :default_options, :required_options
|
|
11
|
+
def valid_parameters(*args)
|
|
12
|
+
@valid_options ||= []
|
|
13
|
+
@valid_options = (@valid_options + args.dup).uniq
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def default_parameters(options = {})
|
|
17
|
+
@default_options ||= {}
|
|
18
|
+
@default_options.merge! options.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def required_parameters(*args)
|
|
22
|
+
valid_parameters(*args)
|
|
23
|
+
@required_options ||= []
|
|
24
|
+
@required_options = (@required_options + args.dup).uniq
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
def initialize(provider, options = {})
|
|
29
|
+
@provider = provider
|
|
30
|
+
@request_options = options.dup
|
|
31
|
+
@options = internalize(options)
|
|
32
|
+
raise OAI::ArgumentException.new unless valid?
|
|
33
|
+
end
|
|
34
|
+
def response
|
|
35
|
+
@builder = Builder::XmlMarkup.new
|
|
36
|
+
@builder.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
|
37
|
+
@builder.tag!('OAI-PMH', header) do
|
|
38
|
+
@builder.responseDate Time.now.utc.xmlschema
|
|
39
|
+
@builder.request(provider.url, (@request_options.merge(:verb => verb) unless self.class == Error))
|
|
40
|
+
yield @builder
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def self.parse_date(value)
|
|
47
|
+
return value if value.respond_to?(:strftime)
|
|
48
|
+
|
|
49
|
+
if value[-1] == "Z"
|
|
50
|
+
Time.strptime(value, "%Y-%m-%dT%H:%M:%S%Z").utc
|
|
51
|
+
else
|
|
52
|
+
Date.strptime(value, "%Y-%m-%d")
|
|
53
|
+
end
|
|
54
|
+
rescue ArgumentError => e
|
|
55
|
+
raise OAI::ArgumentException.new, "unparsable date: '#{value}'"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def header
|
|
61
|
+
{
|
|
62
|
+
'xmlns' => "http://www.openarchives.org/OAI/2.0/",
|
|
63
|
+
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
|
64
|
+
'xsi:schemaLocation' => %{http://www.openarchives.org/OAI/2.0/
|
|
65
|
+
http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd}.gsub(/\s+/, ' ')
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
def extract_identifier(id)
|
|
69
|
+
id.sub("#{provider.prefix}:", '')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid?
|
|
73
|
+
return true if resumption?
|
|
74
|
+
|
|
75
|
+
return true if self.class.valid_options.nil? and options.empty?
|
|
76
|
+
|
|
77
|
+
# check if the request includes an argument and there are no valid
|
|
78
|
+
# arguments for that verb (Identify, for example).
|
|
79
|
+
raise OAI::ArgumentException.new if self.class.valid_options.nil? && !options.empty?
|
|
80
|
+
|
|
81
|
+
if self.class.required_options
|
|
82
|
+
return false unless (self.class.required_options - @options.keys).empty?
|
|
83
|
+
end
|
|
84
|
+
return false unless (@options.keys - self.class.valid_options).empty?
|
|
85
|
+
populate_defaults
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def populate_defaults
|
|
89
|
+
self.class.default_options.each do |k,v|
|
|
90
|
+
@options[k] = v.respond_to?(:call) ? v.call(self) : v if not @options[k]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resumption?
|
|
95
|
+
if @options.keys.include?(:resumption_token)
|
|
96
|
+
return true if 1 == @options.keys.size
|
|
97
|
+
raise OAI::ArgumentException.new
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Convert our internal representations back into standard OAI options
|
|
102
|
+
def externalize(value)
|
|
103
|
+
value.to_s.gsub(/_[a-z]/) { |m| m.sub("_", '').capitalize }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_date(value)
|
|
107
|
+
self.class.parse_date(value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def internalize(hash = {})
|
|
111
|
+
internal = {}
|
|
112
|
+
hash.keys.each do |key|
|
|
113
|
+
internal[key.to_s.gsub(/([A-Z])/, '_\1').downcase.intern] = hash[key].dup
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Convert date formated strings into internal time values
|
|
117
|
+
# Convert date formated strings in dates.
|
|
118
|
+
internal[:from] = parse_date(internal[:from]) if internal[:from]
|
|
119
|
+
internal[:until] = parse_date(internal[:until]) if internal[:until]
|
|
120
|
+
|
|
121
|
+
internal
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def verb
|
|
125
|
+
self.class.to_s.split('::').last
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require File.dirname(__FILE__) + "/partial_result"
|
|
3
|
+
|
|
4
|
+
module OAI::Provider
|
|
5
|
+
# = OAI::Provider::ResumptionToken
|
|
6
|
+
#
|
|
7
|
+
# The ResumptionToken class forms the basis of paging query results. It
|
|
8
|
+
# provides several helper methods for dealing with resumption tokens.
|
|
9
|
+
#
|
|
10
|
+
# OAI-PMH spec does not specify anything about resumptionToken format, they can
|
|
11
|
+
# be purely opaque tokens.
|
|
12
|
+
#
|
|
13
|
+
# Our implementation however encodes everything needed to construct the next page
|
|
14
|
+
# inside the resumption token.
|
|
15
|
+
#
|
|
16
|
+
# == The 'last' component: offset or ID/pk to resume from
|
|
17
|
+
#
|
|
18
|
+
# The `#last` component is an offset or ID to resume from. In the case of it being
|
|
19
|
+
# an ID to resume from, this assumes that ID's are sortable and results are returned
|
|
20
|
+
# in ID order, so that the 'last' ID can be used as the place to resume from.
|
|
21
|
+
#
|
|
22
|
+
# Originally it was assumed that #last was always an integer, but since existing
|
|
23
|
+
# implementations (like ActiveRecordWrapper) used it as an ID, and identifiers and
|
|
24
|
+
# primary keys are _not_ always integers (can be UUID etc), we have expanded to allow
|
|
25
|
+
# any string value.
|
|
26
|
+
#
|
|
27
|
+
# However, for backwards compatibility #last always returns an integer (sometimes 0 if
|
|
28
|
+
# actual last component is not an integer), and #last_str returns the full string version.
|
|
29
|
+
# Trying to change #last itself to be string broke a lot of existing code in this gem
|
|
30
|
+
# in mysterious ways.
|
|
31
|
+
#
|
|
32
|
+
# Also beware that in some cases the value 0/"0" seems to be a special value used
|
|
33
|
+
# to signify some special case. A lot of "code archeology" going on here after significant
|
|
34
|
+
# period of no maintenance to this gem.
|
|
35
|
+
class ResumptionToken
|
|
36
|
+
attr_reader :prefix, :set, :from, :until, :last, :last_str, :expiration, :total
|
|
37
|
+
|
|
38
|
+
# parses a token string and returns a ResumptionToken
|
|
39
|
+
def self.parse(token_string, expiration = nil, total = nil)
|
|
40
|
+
begin
|
|
41
|
+
options = {}
|
|
42
|
+
matches = /(.+):([^ :]+)$/.match(token_string)
|
|
43
|
+
options[:last] = matches.captures[1]
|
|
44
|
+
|
|
45
|
+
parts = matches.captures[0].split('.')
|
|
46
|
+
options[:metadata_prefix] = parts.shift
|
|
47
|
+
parts.each do |part|
|
|
48
|
+
case part
|
|
49
|
+
when /^s/
|
|
50
|
+
options[:set] = part.sub(/^s\(/, '').sub(/\)$/, '')
|
|
51
|
+
when /^f/
|
|
52
|
+
options[:from] = Time.parse(part.sub(/^f\(/, '').sub(/\)$/, '')).localtime
|
|
53
|
+
when /^u/
|
|
54
|
+
options[:until] = Time.parse(part.sub(/^u\(/, '').sub(/\)$/, '')).localtime
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
self.new(options, expiration, total)
|
|
58
|
+
rescue => err
|
|
59
|
+
raise OAI::ResumptionTokenException.new
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# extracts the metadata prefix from a token string
|
|
64
|
+
def self.extract_format(token_string)
|
|
65
|
+
return token_string.split('.')[0]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(options, expiration = nil, total = nil)
|
|
69
|
+
@prefix = options[:metadata_prefix]
|
|
70
|
+
@set = options[:set]
|
|
71
|
+
self.last = options[:last]
|
|
72
|
+
@from = options[:from] if options[:from]
|
|
73
|
+
@until = options[:until] if options[:until]
|
|
74
|
+
@expiration = expiration if expiration
|
|
75
|
+
@total = total if total
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# convenience method for setting the offset of the next set of results
|
|
79
|
+
def next(last)
|
|
80
|
+
self.last = last
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def ==(other)
|
|
85
|
+
prefix == other.prefix and set == other.set and from == other.from and
|
|
86
|
+
self.until == other.until and last == other.last and
|
|
87
|
+
expiration == other.expiration and total == other.total
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# output an xml resumption token
|
|
91
|
+
def to_xml
|
|
92
|
+
xml = Builder::XmlMarkup.new
|
|
93
|
+
xml.resumptionToken(encode_conditions, hash_of_attributes)
|
|
94
|
+
xml.target!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# return a hash containing just the model selection parameters
|
|
98
|
+
def to_conditions_hash
|
|
99
|
+
conditions = {:metadata_prefix => self.prefix }
|
|
100
|
+
conditions[:set] = self.set if self.set
|
|
101
|
+
conditions[:from] = self.from if self.from
|
|
102
|
+
conditions[:until] = self.until if self.until
|
|
103
|
+
conditions
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# return the a string representation of the token minus the offset/ID
|
|
107
|
+
#
|
|
108
|
+
# Q: Why does it eliminate the offset/id "last" on the end? Doesn't fully
|
|
109
|
+
# represent state without it, which is confusing. Not sure, but
|
|
110
|
+
# other code seems to rely on it, tests break if not.
|
|
111
|
+
def to_s
|
|
112
|
+
encode_conditions.gsub(/:\w+?$/, '')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# take care of our logic to store an integer and a str version, for backwards
|
|
118
|
+
# compat where it was assumed to be an integer, as well as supporting string.
|
|
119
|
+
def last=(value)
|
|
120
|
+
@last = value.to_i
|
|
121
|
+
@last_str = value.to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def encode_conditions
|
|
125
|
+
return "" if last_str.nil? || last_str.to_s.strip.eql?("")
|
|
126
|
+
|
|
127
|
+
encoded_token = @prefix.to_s.dup
|
|
128
|
+
encoded_token << ".s(#{set})" if set
|
|
129
|
+
if self.from
|
|
130
|
+
if self.from.respond_to?(:utc)
|
|
131
|
+
encoded_token << ".f(#{self.from.utc.xmlschema})"
|
|
132
|
+
else
|
|
133
|
+
encoded_token << ".f(#{self.from.xmlschema})"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
if self.until
|
|
137
|
+
if self.until.respond_to?(:utc)
|
|
138
|
+
encoded_token << ".u(#{self.until.utc.xmlschema})"
|
|
139
|
+
else
|
|
140
|
+
encoded_token << ".u(#{self.until.xmlschema})"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
encoded_token << ":#{last_str}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def hash_of_attributes
|
|
147
|
+
attributes = {}
|
|
148
|
+
attributes[:completeListSize] = self.total if self.total
|
|
149
|
+
attributes[:expirationDate] = self.expiration.utc.xmlschema if self.expiration
|
|
150
|
+
attributes
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
end
|