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,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)" : " " }
|
|
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
|
|
@@ -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
|
|
@@ -2,15 +2,16 @@ module OAI
|
|
|
2
2
|
|
|
3
3
|
# allows for iteration across a list of records
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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.
|
|
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
|