rubydora 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
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: