aspace_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/lib/aspace_client.rb +15 -0
  3. data/lib/aspace_client/archivesspace_json_schema.rb +210 -0
  4. data/lib/aspace_client/asutils.rb +142 -0
  5. data/lib/aspace_client/client_enum_source.rb +30 -0
  6. data/lib/aspace_client/exceptions.rb +15 -0
  7. data/lib/aspace_client/helpers.rb +0 -0
  8. data/lib/aspace_client/json_schema_concurrency_fix.rb +52 -0
  9. data/lib/aspace_client/json_schema_utils.rb +414 -0
  10. data/lib/aspace_client/jsonmodel.rb +342 -0
  11. data/lib/aspace_client/jsonmodel_client.rb +528 -0
  12. data/lib/aspace_client/jsonmodel_i18n_mixin.rb +77 -0
  13. data/lib/aspace_client/jsonmodel_type.rb +478 -0
  14. data/lib/aspace_client/memoryleak.rb +59 -0
  15. data/lib/aspace_client/schemas/abstract_agent.rb +51 -0
  16. data/lib/aspace_client/schemas/abstract_agent_relationship.rb +12 -0
  17. data/lib/aspace_client/schemas/abstract_archival_object.rb +96 -0
  18. data/lib/aspace_client/schemas/abstract_classification.rb +44 -0
  19. data/lib/aspace_client/schemas/abstract_name.rb +23 -0
  20. data/lib/aspace_client/schemas/abstract_note.rb +13 -0
  21. data/lib/aspace_client/schemas/accession.rb +174 -0
  22. data/lib/aspace_client/schemas/accession_parts_relationship.rb +31 -0
  23. data/lib/aspace_client/schemas/accession_sibling_relationship.rb +31 -0
  24. data/lib/aspace_client/schemas/active_edits.rb +23 -0
  25. data/lib/aspace_client/schemas/advanced_query.rb +12 -0
  26. data/lib/aspace_client/schemas/agent_contact.rb +25 -0
  27. data/lib/aspace_client/schemas/agent_corporate_entity.rb +32 -0
  28. data/lib/aspace_client/schemas/agent_family.rb +29 -0
  29. data/lib/aspace_client/schemas/agent_person.rb +31 -0
  30. data/lib/aspace_client/schemas/agent_relationship_associative.rb +28 -0
  31. data/lib/aspace_client/schemas/agent_relationship_earlierlater.rb +28 -0
  32. data/lib/aspace_client/schemas/agent_relationship_parentchild.rb +26 -0
  33. data/lib/aspace_client/schemas/agent_relationship_subordinatesuperior.rb +26 -0
  34. data/lib/aspace_client/schemas/agent_software.rb +22 -0
  35. data/lib/aspace_client/schemas/archival_object.rb +60 -0
  36. data/lib/aspace_client/schemas/archival_record_children.rb +15 -0
  37. data/lib/aspace_client/schemas/boolean_field_query.rb +13 -0
  38. data/lib/aspace_client/schemas/boolean_query.rb +13 -0
  39. data/lib/aspace_client/schemas/classification.rb +10 -0
  40. data/lib/aspace_client/schemas/classification_term.rb +38 -0
  41. data/lib/aspace_client/schemas/classification_tree.rb +17 -0
  42. data/lib/aspace_client/schemas/collection_management.rb +27 -0
  43. data/lib/aspace_client/schemas/container.rb +29 -0
  44. data/lib/aspace_client/schemas/container_location.rb +19 -0
  45. data/lib/aspace_client/schemas/date.rb +19 -0
  46. data/lib/aspace_client/schemas/date_field_query.rb +14 -0
  47. data/lib/aspace_client/schemas/deaccession.rb +20 -0
  48. data/lib/aspace_client/schemas/defaults.rb +104 -0
  49. data/lib/aspace_client/schemas/digital_object.rb +64 -0
  50. data/lib/aspace_client/schemas/digital_object_component.rb +53 -0
  51. data/lib/aspace_client/schemas/digital_object_tree.rb +19 -0
  52. data/lib/aspace_client/schemas/digital_record_children.rb +15 -0
  53. data/lib/aspace_client/schemas/enumeration.rb +29 -0
  54. data/lib/aspace_client/schemas/enumeration_migration.rb +14 -0
  55. data/lib/aspace_client/schemas/event.rb +88 -0
  56. data/lib/aspace_client/schemas/extent.rb +17 -0
  57. data/lib/aspace_client/schemas/external_document.rb +12 -0
  58. data/lib/aspace_client/schemas/external_id.rb +11 -0
  59. data/lib/aspace_client/schemas/field_query.rb +15 -0
  60. data/lib/aspace_client/schemas/file_version.rb +26 -0
  61. data/lib/aspace_client/schemas/group.rb +17 -0
  62. data/lib/aspace_client/schemas/instance.rb +27 -0
  63. data/lib/aspace_client/schemas/job.rb +57 -0
  64. data/lib/aspace_client/schemas/location.rb +36 -0
  65. data/lib/aspace_client/schemas/location_batch.rb +45 -0
  66. data/lib/aspace_client/schemas/merge_request.rb +48 -0
  67. data/lib/aspace_client/schemas/name_corporate_entity.rb +15 -0
  68. data/lib/aspace_client/schemas/name_family.rb +13 -0
  69. data/lib/aspace_client/schemas/name_form.rb +15 -0
  70. data/lib/aspace_client/schemas/name_person.rb +19 -0
  71. data/lib/aspace_client/schemas/name_software.rb +14 -0
  72. data/lib/aspace_client/schemas/note_abstract.rb +17 -0
  73. data/lib/aspace_client/schemas/note_bibliography.rb +29 -0
  74. data/lib/aspace_client/schemas/note_bioghist.rb +22 -0
  75. data/lib/aspace_client/schemas/note_chronology.rb +28 -0
  76. data/lib/aspace_client/schemas/note_citation.rb +32 -0
  77. data/lib/aspace_client/schemas/note_definedlist.rb +25 -0
  78. data/lib/aspace_client/schemas/note_digital_object.rb +23 -0
  79. data/lib/aspace_client/schemas/note_index.rb +29 -0
  80. data/lib/aspace_client/schemas/note_index_item.rb +25 -0
  81. data/lib/aspace_client/schemas/note_multipart.rb +25 -0
  82. data/lib/aspace_client/schemas/note_orderedlist.rb +27 -0
  83. data/lib/aspace_client/schemas/note_outline.rb +20 -0
  84. data/lib/aspace_client/schemas/note_outline_level.rb +21 -0
  85. data/lib/aspace_client/schemas/note_singlepart.rb +24 -0
  86. data/lib/aspace_client/schemas/note_text.rb +17 -0
  87. data/lib/aspace_client/schemas/permission.rb +15 -0
  88. data/lib/aspace_client/schemas/preference.rb +16 -0
  89. data/lib/aspace_client/schemas/record_tree.rb +17 -0
  90. data/lib/aspace_client/schemas/repository.rb +32 -0
  91. data/lib/aspace_client/schemas/repository_with_agent.rb +14 -0
  92. data/lib/aspace_client/schemas/resource.rb +112 -0
  93. data/lib/aspace_client/schemas/resource_tree.rb +20 -0
  94. data/lib/aspace_client/schemas/rights_statement.rb +35 -0
  95. data/lib/aspace_client/schemas/subject.rb +30 -0
  96. data/lib/aspace_client/schemas/term.rb +16 -0
  97. data/lib/aspace_client/schemas/user.rb +56 -0
  98. data/lib/aspace_client/schemas/user_defined.rb +42 -0
  99. data/lib/aspace_client/schemas/vocabulary.rb +15 -0
  100. data/lib/aspace_client/validations.rb +434 -0
  101. data/lib/aspace_client/validator_cache.rb +47 -0
  102. data/lib/aspace_client/version.rb +3 -0
  103. metadata +244 -0
@@ -0,0 +1,528 @@
1
+ require 'net/http/persistent'
2
+ require 'net/http/post/multipart'
3
+ require 'json'
4
+ require_relative 'exceptions'
5
+
6
+
7
+ module JSONModel
8
+
9
+ # Set the repository that subsequent operations will apply to.
10
+ def self.set_repository(id)
11
+ Thread.current[:selected_repo_id] = id
12
+ end
13
+
14
+
15
+ # The currently selected repository (if any)
16
+ def self.repository
17
+ Thread.current[:selected_repo_id]
18
+ end
19
+
20
+
21
+ # Grab an array of JSON objects from 'uri' and use the 'type_descriptor'
22
+ # property of each object to cast it into a JSONModel.
23
+ def self.all(uri, type_descriptor)
24
+ JSONModel::HTTP.get_json(uri).map do |obj|
25
+ JSONModel(obj[type_descriptor.to_s]).new(obj)
26
+ end
27
+ end
28
+
29
+
30
+ def self.with_repository(id)
31
+ old_repo = Thread.current[:selected_repo_id]
32
+ begin
33
+ self.set_repository(id)
34
+ yield
35
+ ensure
36
+ self.set_repository(old_repo)
37
+ end
38
+ end
39
+
40
+
41
+ def self.backend_url
42
+ if Module.const_defined?(:BACKEND_SERVICE_URL)
43
+ BACKEND_SERVICE_URL
44
+ else
45
+ init_args[:url]
46
+ end
47
+ end
48
+
49
+
50
+ @@error_handlers = []
51
+
52
+ def self.add_error_handler(&block)
53
+ @@error_handlers << block
54
+ end
55
+
56
+ def self.handle_error(err)
57
+ @@error_handlers.each do |handler|
58
+ handler.call(err)
59
+ end
60
+ end
61
+
62
+
63
+ module Notification
64
+ @@notification_handlers = []
65
+
66
+ def self.add_notification_handler(code = nil, &block)
67
+ @@notification_handlers << {:code => code, :block => block}
68
+ end
69
+
70
+ def self.start_background_thread
71
+ Thread.new do
72
+ sequence = 0
73
+
74
+ while true
75
+ begin
76
+ notifications = JSONModel::HTTP::get_json('/notifications',
77
+ :last_sequence => sequence)
78
+
79
+ notifications.each do |notification|
80
+ @@notification_handlers.each do |handler|
81
+ if handler[:code].nil? or handler[:code] == notification["code"]
82
+ begin
83
+ handler[:block].call(notification["code"], notification["params"])
84
+ rescue
85
+ $stderr.puts("ERROR: Failed to handle notification #{notification.inspect}: #{$!}")
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ sequence = notifications.last['sequence']
92
+ rescue
93
+ sleep 5
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+
102
+
103
+ module HTTP
104
+
105
+ def self.backend_url
106
+ if Module.const_defined?(:BACKEND_SERVICE_URL)
107
+ BACKEND_SERVICE_URL
108
+ else
109
+ JSONModel::init_args[:url]
110
+ end
111
+ end
112
+
113
+
114
+ # We override this in the backend's spec_helper since Rack::Test::Methods
115
+ # doesn't support multipart requests.
116
+ def self.multipart_request(uri, params)
117
+ Net::HTTP::Post::Multipart.new(uri, params)
118
+ end
119
+
120
+
121
+ def self.form_urlencoded(uri, params)
122
+ request = Net::HTTP::Post.new(uri)
123
+ request.form_data = params
124
+ request
125
+ end
126
+
127
+
128
+ # Perform a HTTP POST request against the backend with form parameters
129
+ #
130
+ # `encoding' is either :x_www_form_urlencoded or :multipart_form_data. The
131
+ # latter is useful if you're providing a file upload.
132
+ def self.post_form(uri, params = {}, encoding = :x_www_form_urlencoded)
133
+ url = URI("#{backend_url}#{uri}")
134
+
135
+ req = if encoding == :x_www_form_urlencoded
136
+ self.form_urlencoded(url.request_uri, params)
137
+ elsif encoding == :multipart_form_data
138
+ self.multipart_request(url.request_uri, params)
139
+ else
140
+ raise "Unknown form encoding: #{encoding.inspect}"
141
+ end
142
+
143
+ do_http_request(url, req)
144
+ end
145
+
146
+
147
+ def self.stream(uri, params = {}, &block)
148
+ uri = URI("#{backend_url}#{uri}")
149
+ uri.query = URI.encode_www_form(params)
150
+
151
+ req = Net::HTTP::Get.new(uri.request_uri)
152
+
153
+ req['X-ArchivesSpace-Session'] = current_backend_session
154
+
155
+ if high_priority?
156
+ req['X-ArchivesSpace-Priority'] = "high"
157
+ end
158
+
159
+ Net::HTTP.start(uri.host, uri.port) do |http|
160
+ http.request(req, nil) do |response|
161
+ if response.code =~ /^4/
162
+ JSONModel::handle_error(ASUtils.json_parse(response.body))
163
+ raise response.body
164
+ end
165
+
166
+ block.call(response)
167
+ end
168
+ end
169
+ end
170
+
171
+
172
+ def self.get_json(uri, params = {})
173
+ uri = URI("#{backend_url}#{uri}")
174
+ uri.query = URI.encode_www_form(params)
175
+
176
+ response = get_response(uri)
177
+
178
+ if response.is_a?(Net::HTTPSuccess) || response.status == 200
179
+ ASUtils.json_parse(response.body)
180
+ else
181
+ nil
182
+ end
183
+ end
184
+
185
+
186
+ # Returns the session token to be sent to the backend when making
187
+ # requests.
188
+ def self.current_backend_session
189
+ # Set by the ApplicationController
190
+ Thread.current[:backend_session]
191
+ end
192
+
193
+
194
+ def self.current_backend_session=(val)
195
+ Thread.current[:backend_session] = val
196
+ end
197
+
198
+
199
+ def self.high_priority?
200
+ if Thread.current[:request_priority]
201
+ Thread.current[:request_priority] == :high
202
+ else
203
+ JSONModel::init_args[:priority] == :high
204
+ end
205
+ end
206
+
207
+
208
+ def self.http_conn
209
+ @http ||= Net::HTTP::Persistent.new 'jsonmodel_client'
210
+ @http.read_timeout = 1200
211
+ @http
212
+ end
213
+
214
+
215
+ def self.do_http_request(url, req, &block)
216
+ req['X-ArchivesSpace-Session'] = current_backend_session
217
+
218
+ if high_priority?
219
+ req['X-ArchivesSpace-Priority'] = "high"
220
+ end
221
+
222
+ response = http_conn.request(url, req, &block)
223
+
224
+ if response.code =~ /^4/
225
+ JSONModel::handle_error(ASUtils.json_parse(response.body))
226
+ end
227
+
228
+ response
229
+ end
230
+
231
+
232
+ def self.with_request_priority(priority)
233
+ old = Thread.current[:request_priority]
234
+ Thread.current[:request_priority] = priority
235
+ begin
236
+ yield
237
+ ensure
238
+ Thread.current[:request_priority] = old
239
+ end
240
+ end
241
+
242
+
243
+ def self.post_json(url, json)
244
+ req = Net::HTTP::Post.new(url.request_uri)
245
+ req['Content-Type'] = 'text/json'
246
+ req.body = json
247
+
248
+ do_http_request(url, req)
249
+ end
250
+
251
+
252
+ def self.post_json_file(url, path, &block)
253
+ File.open(path) do |fh|
254
+ req = Net::HTTP::Post.new(url.request_uri)
255
+ req['Content-Type'] = 'text/json'
256
+ req['Content-Length'] = File.size(path)
257
+ req.body_stream = fh
258
+
259
+ do_http_request(url, req, &block)
260
+ end
261
+ end
262
+
263
+
264
+ def self.delete_request(url)
265
+ req = Net::HTTP::Delete.new(url.request_uri)
266
+
267
+ do_http_request(url, req)
268
+ end
269
+
270
+
271
+ def self.get_response(url)
272
+ req = Net::HTTP::Get.new(url.request_uri)
273
+
274
+ do_http_request(url, req)
275
+ end
276
+
277
+ end
278
+
279
+
280
+
281
+ module Client
282
+
283
+ def self.included(base)
284
+ base.extend(ClassMethods)
285
+ end
286
+
287
+
288
+ # Validate this JSONModel instance, produce a JSON string, and send an
289
+ # update to the backend.
290
+ def save(opts = {}, whole_body = false)
291
+
292
+ clear_errors
293
+
294
+ type = self.class.record_type
295
+ response = JSONModel::HTTP.post_json(self.class.my_url(self.id, opts),
296
+ self.to_json)
297
+
298
+ if response.code == '200'
299
+ response = ASUtils.json_parse(response.body)
300
+
301
+ self.uri = self.class.uri_for(response["id"], opts)
302
+
303
+ # If we were able to save successfully, increment our local version
304
+ # number to match the version on the server.
305
+ self.lock_version = response["lock_version"]
306
+
307
+ # Ensure object is up to date
308
+ if response["stale"]
309
+ self.refetch
310
+ end
311
+
312
+ return whole_body ? response : response["id"]
313
+
314
+ elsif response.code == '403'
315
+ raise AccessDeniedException.new
316
+
317
+ elsif response.code == '409'
318
+ err = ASUtils.json_parse(response.body)
319
+ raise ConflictException.new(err["error"])
320
+
321
+ elsif response.code == '404'
322
+ raise RecordNotFound.new
323
+
324
+ elsif response.code =~ /^4/
325
+ err = ASUtils.json_parse(response.body)
326
+
327
+ if err["error"].is_a?(Hash)
328
+ err["error"].each do |field, errors|
329
+ errors.each do |msg|
330
+ add_error(field, msg)
331
+ end
332
+ end
333
+ end
334
+
335
+ raise ValidationException.new(:invalid_object => self,
336
+ :errors => err["error"])
337
+ else
338
+ raise Exception.new("Unknown response: #{response.body} (code: #{response.code})")
339
+ end
340
+ end
341
+
342
+
343
+ def refetch
344
+ # if a new object, nothing to fetch
345
+ return self if self.id.nil?
346
+
347
+ obj = (self.instance_data.has_key? :find_opts) ?
348
+ self.class.find(self.id, self.instance_data[:find_opts]) : self.class.find(self.id)
349
+
350
+ self.reset_from(obj) if not obj.nil?
351
+ end
352
+
353
+
354
+ def delete
355
+ response = JSONModel::HTTP.delete_request(self.class.my_url(self.id))
356
+
357
+ if response.code == '200'
358
+ true
359
+ elsif response.code == '403'
360
+ raise AccessDeniedException.new
361
+ elsif response.code == '404'
362
+ nil
363
+ elsif response.code == '409'
364
+ err = ASUtils.json_parse(response.body)
365
+ raise ConflictException.new(err["error"])
366
+ else
367
+ raise Exception.new("Unknown response: #{response}")
368
+ end
369
+ end
370
+
371
+ # Mark the suppression status of this record
372
+ def set_suppressed(val)
373
+ response = JSONModel::HTTP.post_form("#{self.uri}/suppressed", :suppressed => val)
374
+
375
+ if response.code == '403'
376
+ raise AccessDeniedException.new("Permission denied when setting suppression status")
377
+ elsif response.code != '200'
378
+ raise "Error when setting suppression status for #{self}: #{response.code} -- #{response.body}"
379
+ end
380
+
381
+ self["suppressed"] = ASUtils.json_parse(response.body)["suppressed_state"]
382
+ end
383
+
384
+
385
+ def suppress
386
+ set_suppressed(true)
387
+ end
388
+
389
+
390
+ def unsuppress
391
+ set_suppressed(false)
392
+ end
393
+
394
+
395
+ def add_error(field, message)
396
+ @errors ||= {}
397
+ @errors[field.to_s] ||= []
398
+ @errors[field.to_s] << message
399
+ end
400
+
401
+
402
+ module ClassMethods
403
+
404
+ def self.extended(base)
405
+ class << base
406
+ alias :_substitute_parameters :substitute_parameters
407
+
408
+ def substitute_parameters(uri, opts = {})
409
+ opts = ASUtils.keys_as_strings(opts)
410
+ if JSONModel::repository
411
+ opts = {'repo_id' => JSONModel::repository}.merge(opts)
412
+ end
413
+
414
+ _substitute_parameters(uri, opts)
415
+ end
416
+ end
417
+ end
418
+
419
+
420
+ # Given the ID of a JSONModel instance, return its full URL (including the
421
+ # URL of the backend)
422
+ def my_url(id = nil, opts = {})
423
+ uri, remaining_opts = self.uri_and_remaining_options_for(id, opts)
424
+
425
+ url = URI("#{JSONModel::HTTP.backend_url}#{uri}")
426
+
427
+ # Don't need to pass this as a URL parameter if it wasn't picked up by
428
+ # the URI template substitution.
429
+ remaining_opts.delete(:repo_id)
430
+
431
+ if not remaining_opts.empty?
432
+ url.query = URI.encode_www_form(remaining_opts)
433
+ end
434
+
435
+ url
436
+ end
437
+
438
+
439
+ # Given an ID, retrieve an instance of the current JSONModel from the
440
+ # backend.
441
+ def find(id, opts = {})
442
+ response = JSONModel::HTTP.get_response(my_url(id, opts))
443
+
444
+ if response.code == '200'
445
+ obj = self.new(ASUtils.json_parse(response.body))
446
+ # store find params on instance to support #refetch
447
+ obj.instance_data[:find_opts] = opts
448
+ obj
449
+ elsif response.code == '403'
450
+ raise AccessDeniedException.new
451
+ elsif response.code == '404'
452
+ raise RecordNotFound.new
453
+ else
454
+ raise response.body
455
+ end
456
+ end
457
+
458
+
459
+ def find_by_uri(uri, opts = {})
460
+ self.find(self.id_for(uri), opts)
461
+ end
462
+
463
+
464
+ # Return all instances of the current JSONModel's record type.
465
+ def all(params = {}, opts = {})
466
+ uri = my_url(nil, opts)
467
+
468
+ uri.query = URI.encode_www_form(params)
469
+
470
+ response = JSONModel::HTTP.get_response(uri)
471
+
472
+ if response.code == '200'
473
+ json_list = ASUtils.json_parse(response.body)
474
+
475
+ if json_list.is_a?(Hash)
476
+ json_list["results"] = json_list["results"].map {|h| self.new(h)}
477
+ else
478
+ json_list = json_list.map {|h| self.new(h)}
479
+ end
480
+
481
+ json_list
482
+ elsif response.code == '403'
483
+ raise AccessDeniedException.new
484
+ else
485
+ raise response.body
486
+ end
487
+ end
488
+
489
+ end
490
+
491
+
492
+ class EnumSource
493
+
494
+ def self.fetch_enumerations
495
+ enumerations = {}
496
+ enumerations[:defaults] = {}
497
+ JSONModel::JSONModel(:enumeration).all.each do |enumeration|
498
+ enumerations[enumeration.name] = enumeration.values
499
+ enumerations[:defaults][enumeration.name] = enumeration.default_value
500
+ end
501
+
502
+ enumerations
503
+ end
504
+
505
+
506
+ def initialize
507
+ @enumerations = self.class.fetch_enumerations
508
+ end
509
+
510
+
511
+ def valid?(name, value)
512
+ values_for(name).include?(value)
513
+ end
514
+
515
+
516
+ def values_for(name)
517
+ @enumerations.fetch(name)
518
+ end
519
+
520
+ def default_value_for
521
+ @enumerations[:defaults].fetch(name)
522
+ end
523
+
524
+ end
525
+
526
+
527
+ end
528
+ end