rubydora 0.5.10 → 0.5.11

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.
data/History.textile CHANGED
@@ -1,3 +1,23 @@
1
+ h3. 0.5.11
2
+ * Ensure that the base rest resource is insulated from blocks passed to dissemination and datastream_dissemination.
3
+ * Deprecated a behavior for DigitalObject.find that allowed one to call DigitalObject.find for a non-existent object. This will raise an exception in a future rubydora release. Instead, you should use the new DigitalObject.find_or_initialize for that behavior.
4
+ * Use ActiveSupport::Rescuable for the REST API calls.
5
+ * Implement a basic form of "transactions" in Rubydora, intended to be used during testing. It uses the new REST API hooks and knows how to roll-back the different data-changing operations. In some cases, this involves deleting and re-ingesting the previous version of the object (purge, modify datastream, purge datastream), which make it unsuitable for production use, especially with large content datastreams.
6
+
7
+ At Stanford, we've hooked it into our rspec tests as an around filter:
8
+ bc. RSpec.configure do |config|
9
+
10
+ config.around(:each) do |example|
11
+ ActiveFedora::Base.connection_for_pid(0).transaction do |t|
12
+ example.call
13
+ t.rollback
14
+ end
15
+ end
16
+ end
17
+
18
+ h3. 0.5.10
19
+ * Support for content_changed? and content_will_change!
20
+
1
21
  h3. 0.5.9
2
22
  * Raise exception if it's not 404
3
23
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.10
1
+ 0.5.11
@@ -16,6 +16,10 @@ module Rubydora
16
16
  include Rubydora::ModelsMixin
17
17
  include Rubydora::RelationshipsMixin
18
18
 
19
+ extend Deprecation
20
+
21
+ self.deprecation_horizon = 'rubydora 0.6'
22
+
19
23
 
20
24
  attr_reader :pid
21
25
 
@@ -44,7 +48,23 @@ module Rubydora
44
48
  # @param [String] pid
45
49
  # @param [Rubydora::Repository] context
46
50
  def self.find pid, repository = nil, options = {}
47
- self.new pid, repository, options
51
+ obj = self.new pid, repository, options
52
+ if obj.new?
53
+ Deprecation.warn(Rubydora::DigitalObject, "DigitalObject.find called
54
+ for an object that doesn't exist. In #{Rubydora::DigitalObject.deprecation_horizon},
55
+ this behavior will raise an exception. Use
56
+ DigitalObject.new or DigitalObject.find_or_initialize instead.")
57
+ end
58
+
59
+ obj
60
+ end
61
+
62
+ # find or initialize a Fedora object
63
+ # @param [String] pid
64
+ # @param [Rubydora::Repository] repository context
65
+ # @param [Hash] options default attribute values (used esp. for creating new datastreams
66
+ def self.find_or_initialize *args
67
+ self.new *args
48
68
  end
49
69
 
50
70
  # create a new fedora object (see also DigitalObject#save)
@@ -258,10 +278,6 @@ module Rubydora
258
278
  raise "Can't change values on older versions" if @asOfDateTime
259
279
  end
260
280
 
261
- def check_if_read_only
262
- raise "Can't change values on older versions" if @asOfDateTime
263
- end
264
-
265
281
  private
266
282
  def attribute_will_change! *args
267
283
  check_if_read_only
@@ -1,4 +1,7 @@
1
1
  require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/core_ext/class'
3
+ require 'active_support/core_ext/module'
4
+ require 'hooks'
2
5
 
3
6
  module Rubydora
4
7
 
@@ -6,17 +9,42 @@ module Rubydora
6
9
  module RestApiClient
7
10
 
8
11
  include Rubydora::FedoraUrlHelpers
12
+ extend ActiveSupport::Concern
9
13
  include ActiveSupport::Benchmarkable
10
14
 
15
+
11
16
  VALID_CLIENT_OPTIONS = [:user, :password, :timeout, :open_timeout, :ssl_client_cert, :ssl_client_key]
17
+
18
+ included do
19
+ include ActiveSupport::Rescuable
20
+
21
+
22
+ rescue_from RestClient::InternalServerError do |e|
23
+ logger.error e.response
24
+ logger.flush if logger.respond_to? :flush
25
+ raise FedoraInvalidRequest, "See logger for details"
26
+ end
27
+
28
+ rescue_from Errno::ECONNREFUSED, Errno::EHOSTUNREACH do |exception|
29
+ logger.error "Unable to connect to Fedora at #{@client.url}"
30
+ raise exception
31
+ end
32
+
33
+ include Hooks
34
+ [:ingest, :modify_object, :purge_object, :set_datastream_options, :add_datastream, :modify_datastream, :purge_datastream, :add_relationship, :purge_relationship].each do |h|
35
+ define_hook "before_#{h}"
36
+ end
37
+
38
+ define_hook "after_ingest"
39
+ include Transactions
40
+ end
41
+
12
42
  # Create an authorized HTTP client for the Fedora REST API
13
43
  # @param [Hash] config
14
44
  # @option config [String] :url
15
45
  # @option config [String] :user
16
46
  # @option config [String] :password
17
47
  # @return [RestClient::Resource]
18
-
19
-
20
48
  #TODO trap for these errors specifically: RestClient::Request::Unauthorized, Errno::ECONNREFUSED
21
49
  def client config = {}
22
50
  client_config = self.config.merge(config)
@@ -37,13 +65,9 @@ module Rubydora
37
65
  # @return [String]
38
66
  def next_pid options = {}
39
67
  options[:format] ||= 'xml'
40
- begin
41
- return client[next_pid_url(options)].post nil
42
- rescue RestClient::InternalServerError => e
43
- logger.error e.response
44
- logger.flush if logger.respond_to? :flush
45
- raise FedoraInvalidRequest, "Error getting nextPID. See logger for details"
46
- end
68
+ client[next_pid_url(options)].post nil
69
+ rescue Exception => exception
70
+ rescue_with_handler(exception) || raise
47
71
  end
48
72
 
49
73
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -53,17 +77,13 @@ module Rubydora
53
77
  raise ArgumentError,"Cannot have both :terms and :query parameters" if options[:terms] and options[:query]
54
78
  options[:resultFormat] ||= 'xml'
55
79
 
56
- begin
57
- resource = client[find_objects_url(options)]
58
- if block_given?
59
- resource.options[:block_response] = block_response
60
- end
61
- return resource.get
62
- rescue RestClient::InternalServerError => e
63
- logger.error e.response
64
- logger.flush if logger.respond_to? :flush
65
- raise FedoraInvalidRequest, "Error finding objects. See logger for details"
66
- end
80
+ resource = client[find_objects_url(options)]
81
+ if block_given?
82
+ resource.options[:block_response] = block_response
83
+ end
84
+ return resource.get
85
+ rescue Exception => exception
86
+ rescue_with_handler(exception) || raise
67
87
  end
68
88
 
69
89
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -73,15 +93,9 @@ module Rubydora
73
93
  def object options = {}
74
94
  pid = options.delete(:pid)
75
95
  options[:format] ||= 'xml'
76
- begin
77
- return client[object_url(pid, options)].get
78
- rescue RestClient::ResourceNotFound => e
79
- raise e
80
- rescue RestClient::InternalServerError => e
81
- logger.error e.response
82
- logger.flush if logger.respond_to? :flush
83
- raise FedoraInvalidRequest, "Error getting object #{pid}. See logger for details"
84
- end
96
+ client[object_url(pid, options)].get
97
+ rescue Exception => exception
98
+ rescue_with_handler(exception) || raise
85
99
  end
86
100
 
87
101
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -91,16 +105,11 @@ module Rubydora
91
105
  def ingest options = {}
92
106
  pid = options.delete(:pid) || 'new'
93
107
  file = options.delete(:file)
94
- begin
95
- return client[object_url(pid, options)].post file, :content_type => 'text/xml'
96
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
97
- logger.error "Unable to connect to Fedora at #{@client.url}"
98
- raise e
99
- rescue RestClient::InternalServerError => e
100
- logger.error e.response
101
- logger.flush if logger.respond_to? :flush
102
- raise FedoraInvalidRequest, "Error ingesting object #{pid}. See logger for details"
103
- end
108
+ assigned_pid = client[object_url(pid, options)].post file, :content_type => 'text/xml'
109
+ run_hook :after_ingest, :pid => assigned_pid, :file => file, :options => options
110
+ assigned_pid
111
+ rescue Exception => exception
112
+ rescue_with_handler(exception) || raise
104
113
  end
105
114
 
106
115
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -109,12 +118,9 @@ module Rubydora
109
118
  # @return [String]
110
119
  def export options = {}
111
120
  pid = options.delete(:pid)
112
- begin
113
- return client[export_object_url(pid, options)].get
114
- rescue RestClient::InternalServerError => e
115
- logger.error e.response
116
- raise FedoraInvalidRequest, "Error exporting object #{pid}. See logger for details"
117
- end
121
+ client[export_object_url(pid, options)].get
122
+ rescue Exception => exception
123
+ rescue_with_handler(exception) || raise
118
124
  end
119
125
 
120
126
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -123,13 +129,10 @@ module Rubydora
123
129
  # @return [String]
124
130
  def modify_object options = {}
125
131
  pid = options.delete(:pid)
126
- begin
127
- return client[object_url(pid, options)].put nil
128
- rescue RestClient::InternalServerError => e
129
- logger.error e.response
130
- logger.flush if logger.respond_to? :flush
131
- raise FedoraInvalidRequest, "Error modifying object #{pid}. See logger for details"
132
- end
132
+ run_hook :before_modify_object, :pid => pid, :options => options
133
+ client[object_url(pid, options)].put nil
134
+ rescue Exception => exception
135
+ rescue_with_handler(exception) || raise
133
136
  end
134
137
 
135
138
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -138,15 +141,10 @@ module Rubydora
138
141
  # @return [String]
139
142
  def purge_object options = {}
140
143
  pid = options.delete(:pid)
141
- begin
142
- return client[object_url(pid, options)].delete
143
- rescue RestClient::ResourceNotFound => e
144
- raise e
145
- rescue RestClient::InternalServerError => e
146
- logger.error e.response
147
- logger.flush if logger.respond_to? :flush
148
- raise FedoraInvalidRequest, "Error purging object #{pid}. See logger for details"
149
- end
144
+ run_hook :before_purge_object, :pid => pid, :options => options
145
+ client[object_url(pid, options)].delete
146
+ rescue Exception => exception
147
+ rescue_with_handler(exception) || raise
150
148
  end
151
149
 
152
150
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -157,13 +155,9 @@ module Rubydora
157
155
  pid = options.delete(:pid)
158
156
  options[:format] ||= 'xml'
159
157
  raise ArgumentError, "Must have a pid" unless pid
160
- begin
161
- return client[object_versions_url(pid, options)].get
162
- rescue RestClient::InternalServerError => e
163
- logger.error e.response
164
- logger.flush if logger.respond_to? :flush
165
- raise FedoraInvalidRequest, "Error getting versions for object #{pid}. See logger for details"
166
- end
158
+ client[object_versions_url(pid, options)].get
159
+ rescue Exception => exception
160
+ rescue_with_handler(exception) || raise
167
161
  end
168
162
 
169
163
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -174,13 +168,9 @@ module Rubydora
174
168
  pid = options.delete(:pid)
175
169
  raise ArgumentError, "Missing required parameter :pid" unless pid
176
170
  options[:format] ||= 'xml'
177
- begin
178
- return client[object_xml_url(pid, options)].get
179
- rescue RestClient::InternalServerError => e
180
- logger.error e.response
181
- logger.flush if logger.respond_to? :flush
182
- raise FedoraInvalidRequest, "Error getting objectXML for object #{pid}. See logger for details"
183
- end
171
+ client[object_xml_url(pid, options)].get
172
+ rescue Exception => exception
173
+ rescue_with_handler(exception) || raise
184
174
  end
185
175
 
186
176
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -194,26 +184,18 @@ module Rubydora
194
184
  pid = options.delete(:pid)
195
185
  dsid = options.delete(:dsid)
196
186
  options[:format] ||= 'xml'
197
- begin
198
- val = nil
199
- message = dsid.nil? ? "Loaded datastream list for #{pid}" : "Loaded datastream #{pid}/#{dsid}"
200
- benchmark message, :level=>:debug do
201
- val = client[datastream_url(pid, dsid, options)].get
202
- end
203
- return val
204
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
205
- logger.error "Unable to connect to Fedora at #{@client.url}"
206
- raise e
207
- rescue RestClient::ResourceNotFound => e
208
- raise e
209
- rescue RestClient::Unauthorized => e
210
- logger.error "Unauthorized at #{client.url}/#{datastream_url(pid, dsid, options)}"
211
- raise e
212
- rescue RestClient::InternalServerError => e
213
- logger.error e.response
214
- logger.flush if logger.respond_to? :flush
215
- raise FedoraInvalidRequest, "Error getting datastream '#{dsid}' for object #{pid}. See logger for details"
187
+ val = nil
188
+ message = dsid.nil? ? "Loaded datastream list for #{pid}" : "Loaded datastream #{pid}/#{dsid}"
189
+ benchmark message, :level=>:debug do
190
+ val = client[datastream_url(pid, dsid, options)].get
216
191
  end
192
+
193
+ val
194
+ rescue RestClient::Unauthorized => e
195
+ logger.error "Unauthorized at #{client.url}/#{datastream_url(pid, dsid, options)}"
196
+ raise e
197
+ rescue Exception => exception
198
+ rescue_with_handler(exception) || raise
217
199
  end
218
200
 
219
201
  alias_method :datastreams, :datastream
@@ -226,13 +208,10 @@ module Rubydora
226
208
  def set_datastream_options options = {}
227
209
  pid = options.delete(:pid)
228
210
  dsid = options.delete(:dsid)
229
- begin
230
- return client[datastream_url(pid, dsid, options)].put nil
231
- rescue RestClient::InternalServerError => e
232
- logger.error e.response
233
- logger.flush if logger.respond_to? :flush
234
- raise FedoraInvalidRequest, "Error setting datastream options on #{dsid} for object #{pid}. See logger for details"
235
- end
211
+ run_hook :before_set_datastream_options, :pid => pid, :dsid => dsid, :options => options
212
+ client[datastream_url(pid, dsid, options)].put nil
213
+ rescue Exception => exception
214
+ rescue_with_handler(exception) || raise
236
215
  end
237
216
 
238
217
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -245,16 +224,12 @@ module Rubydora
245
224
  dsid = options.delete(:dsid)
246
225
  raise ArgumentError, "Must supply dsid" unless dsid
247
226
  options[:format] ||= 'xml'
248
- begin
249
- return client[datastream_history_url(pid, dsid, options)].get
250
- rescue RestClient::ResourceNotFound => e
251
- #404 Resource Not Found: No datastream history could be found. There is no datastream history for the digital object "changeme:1" with datastream ID of "descMetadata
252
- return nil
253
- rescue RestClient::InternalServerError => e
254
- logger.error e.response
255
- logger.flush if logger.respond_to? :flush
256
- raise FedoraInvalidRequest, "Error getting versions for datastream #{dsid} for object #{pid}. See logger for details"
257
- end
227
+ client[datastream_history_url(pid, dsid, options)].get
228
+ rescue RestClient::ResourceNotFound => e
229
+ #404 Resource Not Found: No datastream history could be found. There is no datastream history for the digital object "changeme:1" with datastream ID of "descMetadata
230
+ return nil
231
+ rescue Exception => exception
232
+ rescue_with_handler(exception) || raise
258
233
  end
259
234
 
260
235
  alias_method :datastream_history, :datastream_versions
@@ -270,19 +245,14 @@ module Rubydora
270
245
  method = options.delete(:method)
271
246
  method ||= :get
272
247
  raise self.class.name + "#datastream_dissemination requires a DSID" unless dsid
273
- begin
248
+ if block_given?
249
+ resource = safe_subresource(datastream_content_url(pid, dsid, options), :block_response => block_response)
250
+ else
274
251
  resource = client[datastream_content_url(pid, dsid, options)]
275
- if block_given?
276
- resource.options[:block_response] = block_response
277
- end
278
- return resource.send(method)
279
- rescue RestClient::ResourceNotFound => e
280
- raise e
281
- rescue RestClient::InternalServerError => e
282
- logger.error e.response
283
- logger.flush if logger.respond_to? :flush
284
- raise FedoraInvalidRequest, "Error getting dissemination for datastream #{dsid} for object #{pid}. See logger for details"
285
252
  end
253
+ resource.send(method)
254
+ rescue Exception => exception
255
+ rescue_with_handler(exception) || raise
286
256
  end
287
257
 
288
258
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -295,13 +265,10 @@ module Rubydora
295
265
  dsid = options.delete(:dsid)
296
266
  file = options.delete(:content)
297
267
  content_type = options.delete(:content_type) || options[:mimeType] || (MIME::Types.type_for(file.path).first if file.respond_to? :path) || 'application/octet-stream'
298
- begin
299
- return client[datastream_url(pid, dsid, options)].post file, :content_type => content_type.to_s, :multipart => true
300
- rescue RestClient::InternalServerError => e
301
- logger.error e.response
302
- logger.flush if logger.respond_to? :flush
303
- raise FedoraInvalidRequest, "Error adding datastream #{dsid} for object #{pid}. See logger for details"
304
- end
268
+ run_hook :before_add_datastream, :pid => pid, :dsid => dsid, :file => file, :options => options
269
+ client[datastream_url(pid, dsid, options)].post file, :content_type => content_type.to_s, :multipart => true
270
+ rescue Exception => exception
271
+ rescue_with_handler(exception) || raise
305
272
  end
306
273
 
307
274
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -321,13 +288,11 @@ module Rubydora
321
288
  rest_client_options[:content_type] = content_type
322
289
  end
323
290
 
324
- begin
325
- return client[datastream_url(pid, dsid, options)].put(file, rest_client_options)
326
- rescue RestClient::InternalServerError => e
327
- logger.error e.response
328
- logger.flush if logger.respond_to? :flush
329
- raise FedoraInvalidRequest, "Error modifying datastream #{dsid} for #{pid}. See logger for details"
330
- end
291
+ run_hook :before_modify_datastream, :pid => pid, :dsid => dsid, :file => file, :content_type => content_type, :options => options
292
+ client[datastream_url(pid, dsid, options)].put(file, rest_client_options)
293
+
294
+ rescue Exception => exception
295
+ rescue_with_handler(exception) || raise
331
296
  end
332
297
 
333
298
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -338,13 +303,10 @@ module Rubydora
338
303
  def purge_datastream options = {}
339
304
  pid = options.delete(:pid)
340
305
  dsid = options.delete(:dsid)
341
- begin
342
- client[datastream_url(pid, dsid, options)].delete
343
- rescue RestClient::InternalServerError => e
344
- logger.error e.response
345
- logger.flush if logger.respond_to? :flush
346
- raise FedoraInvalidRequest, "Error purging datastream #{dsid} for #{pid}. See logger for details"
347
- end
306
+ run_hook :before_purge_datastream, :pid => pid, :dsid => dsid
307
+ client[datastream_url(pid, dsid, options)].delete
308
+ rescue Exception => exception
309
+ rescue_with_handler(exception) || raise
348
310
  end
349
311
 
350
312
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -355,13 +317,9 @@ module Rubydora
355
317
  pid = options.delete(:pid) || options[:subject]
356
318
  raise ArgumentError, "Missing required parameter :pid" unless pid
357
319
  options[:format] ||= 'xml'
358
- begin
359
- return client[object_relationship_url(pid, options)].get
360
- rescue RestClient::InternalServerError => e
361
- logger.error e.response
362
- logger.flush if logger.respond_to? :flush
363
- raise FedoraInvalidRequest, "Error getting relationships for #{pid}. See logger for details"
364
- end
320
+ client[object_relationship_url(pid, options)].get
321
+ rescue Exception => exception
322
+ rescue_with_handler(exception) || raise
365
323
  end
366
324
 
367
325
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -370,13 +328,10 @@ module Rubydora
370
328
  # @return [String]
371
329
  def add_relationship options = {}
372
330
  pid = options.delete(:pid) || options[:subject]
373
- begin
374
- return client[new_object_relationship_url(pid, options)].post nil
375
- rescue RestClient::InternalServerError => e
376
- logger.error e.response
377
- logger.flush if logger.respond_to? :flush
378
- raise FedoraInvalidRequest, "Error adding relationship for #{pid}. See logger for details"
379
- end
331
+ run_hook :before_add_relationship, :pid => pid, :options => options
332
+ client[new_object_relationship_url(pid, options)].post nil
333
+ rescue Exception => exception
334
+ rescue_with_handler(exception) || raise
380
335
  end
381
336
 
382
337
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -385,13 +340,10 @@ module Rubydora
385
340
  # @return [String]
386
341
  def purge_relationship options = {}
387
342
  pid = options.delete(:pid) || options[:subject]
388
- begin
389
- return client[object_relationship_url(pid, options)].delete
390
- rescue RestClient::InternalServerError => e
391
- logger.error e.response
392
- logger.flush if logger.respond_to? :flush
393
- raise FedoraInvalidRequest, "Error purging relationships for #{pid}. See logger for details"
394
- end
343
+ run_hook :before_purge_relationship, :pid => pid, :options => options
344
+ client[object_relationship_url(pid, options)].delete
345
+ rescue Exception => exception
346
+ rescue_with_handler(exception) || raise
395
347
  end
396
348
 
397
349
  # {include:RestApiClient::API_DOCUMENTATION}
@@ -405,16 +357,26 @@ module Rubydora
405
357
  sdef = options.delete(:sdef)
406
358
  method = options.delete(:method)
407
359
  options[:format] ||= 'xml' unless pid and sdef and method
408
- begin
360
+ if block_given?
361
+ resource = safe_subresource(dissemination_url(pid,sdef,method,options), :block_response => block_response)
362
+ else
409
363
  resource = client[dissemination_url(pid,sdef,method,options)]
410
- if block_given?
411
- resource.options[:block_response] = block_response
412
- end
413
- return resource.get
414
- rescue RestClient::InternalServerError => e
415
- logger.error e.response
416
- logger.flush if logger.respond_to? :flush
417
- raise FedoraInvalidRequest, "Error getting dissemination for #{pid}. See logger for details"
364
+ end
365
+ resource.get
366
+
367
+ rescue Exception => exception
368
+ rescue_with_handler(exception) || raise
369
+
370
+ end
371
+
372
+ def safe_subresource(subresource, options=Hash.new)
373
+ url = client.concat_urls(client.url, subresource)
374
+ options = client.options.dup.merge! options
375
+ block = client.block
376
+ if block
377
+ client.class.new(url, options, &block)
378
+ else
379
+ client.class.new(url, options)
418
380
  end
419
381
  end
420
382
  end
@@ -0,0 +1,159 @@
1
+ module Rubydora
2
+ # Extremely basic (and naive) 'transaction' support for Rubydora. This isn't
3
+ # really intended to be used in a production-like situation -- more for
4
+ # rolling back (small) changes during testing.
5
+ module Transactions
6
+ extend ActiveSupport::Concern
7
+
8
+ class << self
9
+ attr_accessor :use_transactions
10
+ end
11
+
12
+ included do
13
+ after_ingest do |options|
14
+ append_to_transactions_log :ingest, options if Rubydora::Transactions.use_transactions
15
+ end
16
+
17
+ before_purge_object do |options|
18
+ append_to_transactions_log :purge_object, :pid => options[:pid], :foxml => export(:pid => options[:pid], :context => :archive) if Rubydora::Transactions.use_transactions
19
+ end
20
+
21
+ before_modify_datastream do |options|
22
+ append_to_transactions_log :modify_datastream, :pid => options[:pid], :foxml => export(:pid => options[:pid], :context => :archive) if Rubydora::Transactions.use_transactions
23
+ end
24
+
25
+ before_purge_datastream do |options|
26
+ append_to_transactions_log :purge_datastream, :pid => options[:pid], :foxml => export(:pid => options[:pid], :context => :archive) if Rubydora::Transactions.use_transactions
27
+ end
28
+
29
+ before_add_datastream do |options|
30
+ append_to_transactions_log :add_datastream, options if Rubydora::Transactions.use_transactions
31
+ end
32
+
33
+ before_add_relationship do |options|
34
+ append_to_transactions_log :add_relationship, options if Rubydora::Transactions.use_transactions
35
+ end
36
+
37
+ before_purge_relationship do |options|
38
+ append_to_transactions_log :purge_relationship, options if Rubydora::Transactions.use_transactions
39
+ end
40
+
41
+ before_modify_object do |options|
42
+ if Rubydora::Transactions.use_transactions
43
+ obj = find(options[:pid])
44
+ append_to_transactions_log :modify_object, :pid => options[:pid], :state => obj.state, :ownerId => obj.ownerId, :logMessage => 'reverting'
45
+ end
46
+ end
47
+
48
+ before_set_datastream_options do |options|
49
+ if Rubydora::Transactions.use_transactions
50
+ obj = find(options[:pid])
51
+ ds = obj.datastreams[options[:dsid]]
52
+
53
+ if options[:options][:versionable]
54
+ append_to_transactions_log :set_datastream_options, :pid => options[:pid], :dsid => options[:dsid], :versionable => ds.versionable
55
+ end
56
+
57
+ if options[:options][:state]
58
+ append_to_transactions_log :set_datastream_options, :pid => options[:pid], :dsid => options[:dsid], :state => ds.state
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+
66
+ # Start a transaction
67
+ def transaction &block
68
+ Transaction.new self, &block
69
+ self.transactions_log.clear
70
+ end
71
+
72
+ # Unshift a transaction entry onto the transaction logs.
73
+ # We want these entries in reverse-chronological order
74
+ # for ease of undoing..
75
+ def append_to_transactions_log *args
76
+ return unless Rubydora::Transactions.use_transactions
77
+ transactions_log.unshift(args)
78
+ end
79
+
80
+ # The repository transaction log.
81
+ def transactions_log
82
+ @log ||= []
83
+ end
84
+ end
85
+
86
+ class Transaction
87
+ attr_reader :repository
88
+ def initialize repository, &block
89
+ @repository = repository
90
+ with_transactions(&block)
91
+ end
92
+
93
+ def with_transactions &block
94
+ old_state = Rubydora::Transactions.use_transactions
95
+ Rubydora::Transactions.use_transactions = true
96
+
97
+ yield(self)
98
+
99
+ Rubydora::Transactions.use_transactions = old_state
100
+ end
101
+
102
+ def without_transactions &block
103
+ old_state = Rubydora::Transactions.use_transactions
104
+ Rubydora::Transactions.use_transactions = false
105
+
106
+ yield(self)
107
+
108
+ Rubydora::Transactions.use_transactions = old_state
109
+ end
110
+
111
+ # Roll-back transactions by reversing their outcomes
112
+ # (or, in some cases, re-ingesting the object at the
113
+ # previous state.
114
+ def rollback
115
+ without_transactions do
116
+ repository.transactions_log.delete_if do |(method, options)|
117
+
118
+ begin
119
+ case method
120
+ when :ingest
121
+ repository.purge_object :pid => options[:pid]
122
+
123
+ when :modify_object
124
+ repository.modify_object options
125
+
126
+ when :add_datastream
127
+ repository.purge_datastream :pid => options[:pid], :dsid => options[:dsid]
128
+
129
+ when :add_relationship
130
+ repository.purge_relationship options[:options].merge(:pid => options[:pid])
131
+
132
+ when :purge_relationship
133
+ repository.add_relationship options[:options].merge(:pid => options[:pid])
134
+
135
+ when :purge_object
136
+ repository.ingest :pid => options[:pid], :file => options[:foxml]
137
+
138
+ when :set_datastream_options
139
+ repository.set_datastream_options options
140
+
141
+ when :modify_datastream
142
+ repository.purge_object :pid => options[:pid] rescue nil
143
+ repository.ingest :pid => options[:pid], :file => options[:foxml]
144
+
145
+ when :purge_datastream
146
+ repository.purge_object :pid => options[:pid] rescue nil
147
+ repository.ingest :pid => options[:pid], :file => options[:foxml]
148
+ end
149
+ rescue
150
+ # no-op
151
+ end
152
+
153
+ end
154
+ end
155
+ true
156
+ end
157
+ end
158
+
159
+ end
data/lib/rubydora.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # Fedora Commons REST API module
2
2
  require 'active_model'
3
+ require 'deprecation'
3
4
 
4
5
  module Rubydora
5
6
  autoload :Datastream, "rubydora/datastream"
@@ -15,6 +16,7 @@ module Rubydora
15
16
  autoload :ExtensionParameters, "rubydora/extension_parameters"
16
17
  autoload :Callbacks, "rubydora/callbacks"
17
18
  autoload :ArrayWithCallback, "rubydora/array_with_callback"
19
+ autoload :Transactions, "rubydora/transactions"
18
20
 
19
21
 
20
22
  require 'csv'
data/rubydora.gemspec CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency "activesupport"
24
24
  s.add_dependency "activemodel"
25
25
  s.add_dependency "savon"
26
+ s.add_dependency "hooks"
27
+ s.add_dependency "deprecation"
26
28
 
27
29
  s.add_development_dependency("rake")
28
30
  s.add_development_dependency("shoulda")
@@ -15,6 +15,23 @@ describe "Integration testing against a live Fedora repository", :integration =>
15
15
  @repository.ping.should == true
16
16
  end
17
17
 
18
+ it "should ingest from foxml" do
19
+ @repository.find('changeme:n').delete rescue nil
20
+ pid = @repository.ingest :pid => 'changeme:n'
21
+
22
+ pid.should == 'changeme:n'
23
+
24
+ obj = @repository.find(pid)
25
+ obj.should_not be_new
26
+ end
27
+
28
+ it "should ingest from foxml" do
29
+ pid = @repository.ingest :pid => 'new'
30
+
31
+ obj = @repository.find(pid)
32
+ obj.should_not be_new
33
+ @repository.find(pid).delete rescue nil
34
+ end
18
35
 
19
36
  it "should create an object" do
20
37
  obj = @repository.find('test:1')
@@ -138,6 +155,101 @@ describe "Integration testing against a live Fedora repository", :integration =>
138
155
  obj.datastreams["my_ds"].mimeType.should == "application/x-text"
139
156
  end
140
157
 
158
+ describe "with transactions" do
159
+ it "should work on ingest" do
160
+ @repository.find('transactions:1').delete rescue nil
161
+
162
+ @repository.transaction do |t|
163
+ obj = @repository.find('transactions:1')
164
+ obj.save
165
+
166
+ t.rollback
167
+ end
168
+
169
+ obj = @repository.find('transactions:1')
170
+ obj.should be_new
171
+ end
172
+
173
+ it "should work on purge" do
174
+ @repository.find('transactions:1').delete rescue nil
175
+
176
+ obj = @repository.find('transactions:1')
177
+ obj.save
178
+
179
+ @repository.transaction do |t|
180
+ obj.delete
181
+
182
+ t.rollback
183
+ end
184
+
185
+ obj = @repository.find('transactions:1')
186
+ obj.should_not be_new
187
+ end
188
+
189
+ it "should work on datastreams" do
190
+ @repository.find('transactions:1').delete rescue nil
191
+
192
+ obj = @repository.find('transactions:1')
193
+ obj.save
194
+
195
+ ds = obj.datastreams['datastream_to_delete']
196
+ ds.content = 'asdf'
197
+ ds.save
198
+
199
+ ds2 = obj.datastreams['datastream_to_change']
200
+ ds2.content = 'asdf'
201
+ ds2.save
202
+
203
+ ds3 = obj.datastreams['datastream_to_change_properties']
204
+ ds3.content = 'asdf'
205
+ ds3.versionable = true
206
+ ds3.dsState = 'I'
207
+ ds3.save
208
+
209
+ @repository.transaction do |t|
210
+ ds.delete
211
+
212
+ ds2.content = '1234'
213
+ ds2.save
214
+
215
+ @repository.set_datastream_options :pid => obj.pid, :dsid => 'datastream_to_change_properties', :state => 'A'
216
+ @repository.set_datastream_options :pid => obj.pid, :dsid => 'datastream_to_change_properties', :versionable => false
217
+
218
+ ds4 = obj.datastreams['datastream_to_create']
219
+ ds4.content = 'asdf'
220
+ ds4.save
221
+
222
+ t.rollback
223
+ end
224
+
225
+ obj = @repository.find('transactions:1')
226
+ obj.datastreams.keys.should_not include('datsatream_to_create')
227
+ obj.datastreams.keys.should include('datastream_to_delete')
228
+ obj.datastreams['datastream_to_change'].content.should == 'asdf'
229
+ obj.datastreams['datastream_to_change_properties'].versionable.should == true
230
+ obj.datastreams['datastream_to_change_properties'].dsState.should == 'I'
231
+ end
232
+
233
+ it "should work on relationships" do
234
+ @repository.find('transactions:1').delete rescue nil
235
+
236
+ obj = @repository.find('transactions:1')
237
+ obj.save
238
+ @repository.add_relationship :subject => obj.pid, :predicate => 'uri:asdf', :object => 'fedora:object'
239
+
240
+ ds = obj.datastreams['RELS-EXT'].content
241
+
242
+ @repository.transaction do |t|
243
+ @repository.purge_relationship :subject => obj.pid, :predicate => 'uri:asdf', :object => 'fedora:object'
244
+ @repository.add_relationship :subject => obj.pid, :predicate => 'uri:qwerty', :object => 'fedora:object'
245
+
246
+ t.rollback
247
+
248
+ end
249
+ obj = @repository.find('transactions:1')
250
+ obj.datastreams['RELS-EXT'].content.should == ds
251
+ end
252
+ end
141
253
 
142
254
  describe "object versions" do
143
255
  it "should have versions" do
@@ -1,15 +1,52 @@
1
1
  require 'spec_helper'
2
- require 'loggable'
3
2
 
4
3
  describe Rubydora::RestApiClient do
4
+ class FakeException < Exception
5
+
6
+ end
5
7
  class MockRepository
6
8
  include Rubydora::RestApiClient
7
9
  include Loggable
8
10
 
9
-
10
11
  attr_accessor :config
11
12
  end
12
13
 
14
+
15
+
16
+ describe "exception handling" do
17
+
18
+ shared_examples "RestClient error handling" do
19
+ subject {
20
+ mock_repository = MockRepository.new
21
+ mock_repository.config = { :url => 'http://example.org' }
22
+
23
+ mock_repository
24
+ }
25
+
26
+ it "should replace a RestClient exception with a Rubydora one" do
27
+ subject.stub_chain(:client, :[], :get).and_raise RestClient::InternalServerError.new
28
+ subject.stub_chain(:client, :[], :put).and_raise RestClient::InternalServerError.new
29
+ subject.stub_chain(:client, :[], :delete).and_raise RestClient::InternalServerError.new
30
+ subject.stub_chain(:client, :[], :post).and_raise RestClient::InternalServerError.new
31
+ expect { subject.send(method, :pid => 'fake:pid', :dsid => 'my_dsid') }.to raise_error Rubydora::FedoraInvalidRequest
32
+ end
33
+ end
34
+
35
+ [:next_pid, :find_objects, :object, :ingest, :export, :modify_object, :purge_object, :object_versions, :object_xml, :datastream, :datastreams, :set_datastream_options, :datastream_versions, :datastream_history, :datastream_dissemination, :add_datastream, :modify_datastream, :purge_datastream, :relationships, :add_relationship, :purge_relationship, :dissemination].each do |method|
36
+
37
+ class_eval %Q{
38
+ describe "##{method}" do
39
+ it_behaves_like "RestClient error handling"
40
+ let(:method) { '#{method}' }
41
+ end
42
+ }
43
+ end
44
+
45
+ end
46
+
47
+
48
+
49
+
13
50
  before(:each) do
14
51
  @fedora_user = 'fedoraAdmin'
15
52
  @fedora_password = 'fedoraAdmin'
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rubydora::Transactions do
4
+
5
+
6
+ subject {
7
+ Rubydora::Repository.any_instance.stub(:version).and_return(100)
8
+ repository = Rubydora::Repository.new :url => 'http://example.org'
9
+ }
10
+
11
+
12
+ describe "#rollback" do
13
+ it "ingest" do
14
+ subject.client.stub_chain(:[], :post).and_return 'asdf'
15
+ subject.should_receive(:purge_object).with(hash_including(:pid => 'asdf'))
16
+
17
+ subject.transaction do |t|
18
+
19
+ subject.ingest :pid => 'asdf', :file => '<a />'
20
+
21
+ t.rollback
22
+ end
23
+ end
24
+
25
+ it "modify_object" do
26
+ subject.client.stub_chain(:[], :put).and_return 'asdf'
27
+
28
+ mock_object = double('Rubydora::DigitalObject', :state => 'A', :ownerId => '567', :logMessage => 'dfghj')
29
+ subject.should_receive(:find).with('asdf').and_return mock_object
30
+
31
+
32
+ subject.transaction do |t|
33
+ subject.modify_object :pid => 'asdf', :state => 'I', :ownerId => '123', :logMessage => 'changing asdf'
34
+
35
+ subject.should_receive(:modify_object).with(hash_including(:pid => 'asdf', :state => 'A', :ownerId => '567', :logMessage => 'reverting'))
36
+ t.rollback
37
+ end
38
+ end
39
+
40
+ it "purge_object" do
41
+ subject.client.stub_chain(:[], :delete)
42
+
43
+ subject.should_receive(:export).with(hash_including(:pid => 'asdf', :context => :archive)).and_return '<xml />'
44
+ subject.should_receive(:ingest).with(hash_including(:pid => 'asdf', :file => '<xml />'))
45
+
46
+ subject.transaction do |t|
47
+ subject.purge_object :pid => 'asdf'
48
+
49
+ t.rollback
50
+ end
51
+ end
52
+
53
+ it "add_datastream" do
54
+ subject.client.stub_chain(:[], :post)
55
+ subject.should_receive(:purge_datastream).with(hash_including(:pid => 'asdf', :dsid => 'mydsid'))
56
+
57
+ subject.transaction do |t|
58
+ subject.add_datastream :pid => 'asdf', :dsid => 'mydsid'
59
+
60
+ t.rollback
61
+ end
62
+ end
63
+
64
+ it "modify_datastream" do
65
+ subject.client.stub_chain(:[], :put)
66
+
67
+ subject.should_receive(:export).with(hash_including(:pid => 'asdf', :context => :archive)).and_return '<xml />'
68
+ subject.should_receive(:ingest).with(hash_including(:pid => 'asdf', :file => '<xml />'))
69
+
70
+ subject.transaction do |t|
71
+ subject.modify_datastream :pid => 'asdf', :dsid => 'mydsid'
72
+
73
+ t.rollback
74
+ end
75
+ end
76
+
77
+ it "set_datastream_options" do
78
+ subject.client.stub_chain(:[], :put)
79
+
80
+ mock_object = double('Rubydora::DigitalObject')
81
+ mock_object.stub_chain(:datastreams, :[], :versionable).and_return(false)
82
+ subject.should_receive(:find).with('asdf').and_return mock_object
83
+
84
+ subject.transaction do |t|
85
+ subject.set_datastream_options :pid => 'asdf', :dsid => 'mydsid', :versionable => true
86
+
87
+ subject.should_receive(:set_datastream_options).with(hash_including(:pid => 'asdf', :versionable => false, :dsid => 'mydsid'))
88
+
89
+ t.rollback
90
+ end
91
+ end
92
+
93
+ it "purge_datastream" do
94
+ subject.client.stub_chain(:[], :delete)
95
+
96
+ subject.should_receive(:export).with(hash_including(:pid => 'asdf', :context => :archive)).and_return '<xml />'
97
+ subject.should_receive(:ingest).with(hash_including(:pid => 'asdf', :file => '<xml />'))
98
+
99
+ subject.transaction do |t|
100
+ subject.purge_datastream :pid => 'asdf', :dsid => 'mydsid'
101
+
102
+ t.rollback
103
+ end
104
+ end
105
+
106
+ it "add_relationship" do
107
+ subject.client.stub_chain(:[], :post)
108
+ subject.should_receive(:purge_relationship).with(hash_including(:subject => 'subject', :predicate => 'predicate', :object => 'object'))
109
+
110
+ subject.transaction do |t|
111
+ subject.add_relationship :subject => 'subject', :predicate => 'predicate', :object => 'object'
112
+ t.rollback
113
+ end
114
+ end
115
+
116
+ it "purge_relationship" do
117
+ subject.client.stub_chain(:[], :delete)
118
+ subject.should_receive(:add_relationship).with(hash_including(:subject => 'subject', :predicate => 'predicate', :object => 'object'))
119
+
120
+ subject.transaction do |t|
121
+ subject.purge_relationship :subject => 'subject', :predicate => 'predicate', :object => 'object'
122
+ t.rollback
123
+ end
124
+ end
125
+ end
126
+ end
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,7 @@ if ENV['COVERAGE'] and RUBY_VERSION =~ /^1.9/
11
11
  end
12
12
 
13
13
  require 'rspec/autorun'
14
+ require 'loggable'
14
15
  require 'rubydora'
15
16
 
16
17
  RSpec.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydora
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.10
4
+ version: 0.5.11
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-25 00:00:00.000000000 Z
12
+ date: 2012-07-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fastercsv
@@ -123,6 +123,38 @@ dependencies:
123
123
  - - ! '>='
124
124
  - !ruby/object:Gem::Version
125
125
  version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: hooks
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: deprecation
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
126
158
  - !ruby/object:Gem::Dependency
127
159
  name: rake
128
160
  requirement: !ruby/object:Gem::Requirement
@@ -251,6 +283,7 @@ files:
251
283
  - lib/rubydora/rest_api_client.rb
252
284
  - lib/rubydora/rest_api_client/v33.rb
253
285
  - lib/rubydora/soap.rb
286
+ - lib/rubydora/transactions.rb
254
287
  - lib/rubydora/version.rb
255
288
  - rubydora.gemspec
256
289
  - spec/lib/datastream_spec.rb
@@ -263,6 +296,7 @@ files:
263
296
  - spec/lib/resource_index_spec.rb
264
297
  - spec/lib/rest_api_client_spec.rb
265
298
  - spec/lib/soap_spec.rb
299
+ - spec/lib/transactions_spec.rb
266
300
  - spec/spec_helper.rb
267
301
  homepage: http://github.com/cbeer/rubydora
268
302
  licenses: []
@@ -299,5 +333,6 @@ test_files:
299
333
  - spec/lib/resource_index_spec.rb
300
334
  - spec/lib/rest_api_client_spec.rb
301
335
  - spec/lib/soap_spec.rb
336
+ - spec/lib/transactions_spec.rb
302
337
  - spec/spec_helper.rb
303
338
  has_rdoc: