dm-persevere-adapter 0.71.4 → 0.72.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,544 @@
1
+ module DataMapper
2
+ module Persevere
3
+ class Adapter < DataMapper::Adapters::AbstractAdapter
4
+ extend Chainable
5
+ extend Deprecate
6
+
7
+ RESERVED_CLASSNAMES = ['User','Transaction','Capability','File','Class', 'Object', 'Versioned']
8
+
9
+
10
+ # Default types for all data object based adapters.
11
+ #
12
+ # @return [Hash] default types for data objects adapters.
13
+ #
14
+ # @api private
15
+ chainable do
16
+ def type_map
17
+ length = DataMapper::Property::String::DEFAULT_LENGTH
18
+ precision = DataMapper::Property::Numeric::DEFAULT_PRECISION
19
+ scale = DataMapper::Property::Decimal::DEFAULT_SCALE
20
+
21
+ @type_map ||= {
22
+ Property::Serial => { :primitive => 'integer' },
23
+ Property::Boolean => { :primitive => 'boolean' },
24
+ Integer => { :primitive => 'integer'},
25
+ String => { :primitive => 'string'},
26
+ Class => { :primitive => 'string'},
27
+ BigDecimal => { :primitive => 'number'},
28
+ Float => { :primitive => 'number'},
29
+ DateTime => { :primitive => 'string', :format => 'date-time'},
30
+ Date => { :primitive => 'string', :format => 'date'},
31
+ Time => { :primitive => 'string', :format => 'time'},
32
+ TrueClass => { :primitive => 'boolean'},
33
+ Property::Text => { :primitive => 'string'},
34
+ DataMapper::Property::Object => { :primitive => 'string'},
35
+ DataMapper::Property::URI => { :primitive => 'string', :format => 'uri'}
36
+ }.freeze
37
+ end
38
+ end
39
+
40
+ # This should go away when we have more methods exposed to retrieve versioned data (and schemas)
41
+ attr_accessor :persevere
42
+
43
+ ##
44
+ # Used by DataMapper to put records into a data-store: "INSERT"
45
+ # in SQL-speak. It takes an array of the resources (model
46
+ # instances) to be saved. Resources each have a key that can be
47
+ # used to quickly look them up later without searching, if the
48
+ # adapter supports it.
49
+ #
50
+ # @param [Array<DataMapper::Resource>] resources
51
+ # The set of resources (model instances)
52
+ #
53
+ # @return [Integer]
54
+ # The number of records that were actually saved into the
55
+ # data-store
56
+ #
57
+ # @api semipublic
58
+ def create(resources)
59
+ connect if @persevere.nil?
60
+ created = 0
61
+
62
+ check_schemas
63
+
64
+ resources.each do |resource|
65
+ resource = Persevere.enhance(resource)
66
+ serial = resource.model.serial(self.name)
67
+ path = "/#{resource.model.storage_name}/"
68
+ # Invoke to_json_hash with a boolean to indicate this is a create
69
+ # We might want to make this a post-to_json_hash cleanup instead
70
+ payload = resource.to_json_hash.delete_if{|key,value| value.nil? }
71
+ DataMapper.logger.debug("(Create) PATH/PAYLOAD: #{path} #{payload.inspect}")
72
+ response = @persevere.create(path, payload)
73
+
74
+ # Check the response, this needs to be more robust and raise
75
+ # exceptions when there's a problem
76
+ if response.code == "201"# good:
77
+ rsrc_hash = JSON.parse(response.body)
78
+ # Typecast attributes, DM expects them properly cast
79
+ resource.model.properties.each do |prop|
80
+ value = rsrc_hash[prop.field.to_s]
81
+ rsrc_hash[prop.field.to_s] = prop.typecast(value) unless value.nil?
82
+ # Shift date/time objects to the correct timezone because persevere is UTC
83
+ case prop
84
+ when DateTime then rsrc_hash[prop.field.to_s] = value.new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24))
85
+ when Time then rsrc_hash[prop.field.to_s] = value.getlocal
86
+ end
87
+ end
88
+
89
+ serial.set!(resource, rsrc_hash["id"]) unless serial.nil?
90
+
91
+ created += 1
92
+ else
93
+ return false
94
+ end
95
+ end
96
+
97
+ # Return the number of resources created in persevere.
98
+ return created
99
+ end
100
+
101
+ ##
102
+ # Used by DataMapper to update the attributes on existing
103
+ # records in a data-store: "UPDATE" in SQL-speak. It takes a
104
+ # hash of the attributes to update with, as well as a query
105
+ # object that specifies which resources should be updated.
106
+ #
107
+ # @param [Hash] attributes
108
+ # A set of key-value pairs of the attributes to update the
109
+ # resources with.
110
+ # @param [DataMapper::Query] query
111
+ # The query that should be used to find the resource(s) to
112
+ # update.
113
+ #
114
+ # @return [Integer]
115
+ # the number of records that were successfully updated
116
+ #
117
+ # @api semipublic
118
+ def update(attributes, query)
119
+ connect if @persevere.nil?
120
+ updated = 0
121
+
122
+ check_schemas
123
+
124
+ if ! query.is_a?(DataMapper::Query)
125
+ resources = [query].flatten
126
+ else
127
+ resources = read_many(query)
128
+ end
129
+
130
+ resources.each do |resource|
131
+ resource = Persevere.enhance(resource)
132
+ tblname = resource.model.storage_name
133
+ path = "/#{tblname}/#{resource.key.first}"
134
+ payload = resource.to_json_hash
135
+ DataMapper.logger.debug("(Update) PATH/PAYLOAD: #{path} #{payload.inspect}")
136
+ result = @persevere.update(path, payload)
137
+
138
+ if result.code == "200"
139
+ updated += 1
140
+ else
141
+ return false
142
+ end
143
+ end
144
+ return updated
145
+ end
146
+
147
+ ##
148
+ # Looks up a collection of records from the data-store: "SELECT"
149
+ # in SQL. Used by Model#all to search for a set of records;
150
+ # that set is in a DataMapper::Collection object.
151
+ #
152
+ # @param [DataMapper::Query] query
153
+ # The query to be used to seach for the resources
154
+ #
155
+ # @return [DataMapper::Collection]
156
+ # A collection of all the resources found by the query.
157
+ #
158
+ # @api semipublic
159
+ def read(query)
160
+ connect if @persevere.nil?
161
+ query = Persevere.enhance(query)
162
+
163
+ resources = Array.new
164
+ tblname = query.model.storage_name
165
+
166
+ json_query, headers = query.to_json_query
167
+
168
+ path = "/#{tblname}/#{json_query}"
169
+ DataMapper.logger.debug("--> PATH/QUERY/HEADERS: #{path} #{headers.inspect}")
170
+
171
+ response = @persevere.retrieve(path, headers)
172
+
173
+ if response.code.match(/20?/)
174
+ results = JSON.parse(response.body)
175
+ results.each do |rsrc_hash|
176
+ # Typecast attributes, DM expects them properly cast
177
+ query.fields.each do |prop|
178
+ object_reference = false
179
+ pname = prop.field.to_s
180
+
181
+ value = rsrc_hash[pname]
182
+ # Dereference references
183
+ unless value.nil?
184
+ if prop.field == 'id'
185
+ rsrc_hash[pname] = prop.typecast(value.to_s.match(/(#{tblname})?\/?([a-zA-Z0-9_-]+$)/)[2])
186
+ else
187
+ rsrc_hash[pname] = prop.typecast(value)
188
+ end
189
+ end
190
+ # Shift date/time objects to the correct timezone because persevere is UTC
191
+ case prop
192
+ when DateTime then rsrc_hash[pname] = value.new_offset(Rational(Time.now.getlocal.gmt_offset/3600, 24))
193
+ when Time then rsrc_hash[pname] = value.getlocal
194
+ end
195
+ end
196
+ end
197
+ resources = query.model.load(results, query)
198
+ end
199
+ # We could almost elimate this if regexp was working in persevere.
200
+
201
+ # This won't work if the RegExp is nested more then 1 layer deep.
202
+ if query.conditions.class == DataMapper::Query::Conditions::AndOperation
203
+ regexp_conds = query.conditions.operands.select do |obj|
204
+ obj.is_a?(DataMapper::Query::Conditions::RegexpComparison) ||
205
+ ( obj.is_a?(DataMapper::Query::Conditions::NotOperation) && obj.operand.is_a?(DataMapper::Query::Conditions::RegexpComparison) )
206
+ end
207
+ regexp_conds.each{|cond| resources = resources.select{|resource| cond.matches?(resource)} }
208
+
209
+ end
210
+
211
+ # query.match_records(resources)
212
+ resources
213
+ end
214
+
215
+ ##
216
+ # Destroys all the records matching the given query. "DELETE" in SQL.
217
+ #
218
+ # @param [DataMapper::Query] query
219
+ # The query used to locate the resources to be deleted.
220
+ #
221
+ # @return [Integer]
222
+ # The number of records that were deleted.
223
+ #
224
+ # @api semipublic
225
+ def delete(query)
226
+
227
+ connect if @persevere.nil?
228
+
229
+ deleted = 0
230
+
231
+ if ! query.is_a?(DataMapper::Query)
232
+ resources = [query].flatten
233
+ else
234
+ resources = read_many(query)
235
+ end
236
+
237
+ resources.each do |resource|
238
+ tblname = resource.model.storage_name
239
+ id = resource.attributes(:field)['id']
240
+
241
+ # Retrieve the ID from persever if the resource doesn't have an ID field
242
+ if id.nil?
243
+ query = Persevere.enhance(resource.query)
244
+ path = "/#{tblname}/#{query.to_json_query_filter}[={'id':id}]"
245
+ response = @persevere.retrieve(path, {})
246
+ id = JSON.parse(response.body)[0]['id'].match(/(\w+\/)*(\d+)/)[2]
247
+ end
248
+
249
+ path = "/#{tblname}/#{id}"
250
+ # path = "/#{tblname}/#{resource.key.first}"
251
+
252
+ DataMapper.logger.debug("(Delete) PATH/QUERY: #{path}")
253
+
254
+ result = @persevere.delete(path)
255
+
256
+ if result.code == "204" # ok
257
+ deleted += 1
258
+ end
259
+ end
260
+ return deleted
261
+ end
262
+
263
+ ##
264
+ #
265
+ # Other methods for the Yogo Data Management Toolkit
266
+ #
267
+ ##
268
+ def get_schema(name = nil, project = nil)
269
+ path = nil
270
+ single = false
271
+
272
+ if name.nil? & project.nil?
273
+ path = "/Class/"
274
+ elsif project.nil?
275
+ path = "/Class/#{name}"
276
+ elsif name.nil?
277
+ path = "/Class/#{project}/"
278
+ else
279
+ path = "/Class/#{project}/#{name}"
280
+ end
281
+ result = @persevere.retrieve(path)
282
+ if result.code == "200"
283
+ schemas = [JSON.parse(result.body)].flatten.select{ |schema| not RESERVED_CLASSNAMES.include?(schema['id']) }
284
+ schemas.each do |schema|
285
+ if schema.has_key?('properties')
286
+ schema['properties']['id'] = { 'type' => "serial", 'index' => true }
287
+ end
288
+ end
289
+
290
+ return name.nil? ? schemas : schemas[0..0]
291
+ else
292
+ return false
293
+ end
294
+ end
295
+
296
+ ##
297
+ #
298
+ def put_schema(schema_hash, project = nil)
299
+ path = "/Class/"
300
+ if ! project.nil?
301
+ if schema_hash.has_key?("id")
302
+ if ! schema_hash['id'].index(project)
303
+ schema_hash['id'] = "#{project}/#{schema_hash['id']}"
304
+ end
305
+ else
306
+ DataMapper.logger.error("You need an id key/value in the hash")
307
+ end
308
+ end
309
+
310
+ properties = schema_hash.delete('properties')
311
+ schema_hash['extends'] = { "$ref" => "/Class/Versioned" } if @options[:versioned]
312
+ schema_hash.delete_if{|key,value| value.nil? }
313
+ result = @persevere.create(path, schema_hash)
314
+ if result.code == '201'
315
+ # return JSON.parse(result.body)
316
+ schema_hash['properties'] = properties
317
+ return update_schema(schema_hash)
318
+ else
319
+ return false
320
+ end
321
+ end
322
+
323
+ ##
324
+ #
325
+ def update_schema(schema_hash, project = nil)
326
+ id = schema_hash['id']
327
+ payload = schema_hash.reject{|key,value| key.to_sym.eql?(:id) }
328
+ payload['extends'] = { "$ref" => "/Class/Versioned" } if @options[:versioned]
329
+
330
+ if project.nil?
331
+ path = "/Class/#{id}"
332
+ else
333
+ path = "/Class/#{project}/#{id}"
334
+ end
335
+
336
+ result = @persevere.update(path, payload)
337
+
338
+ if result.code == '200'
339
+ return result.body
340
+ else
341
+ return false
342
+ end
343
+ end
344
+
345
+ ##
346
+ #
347
+ def delete_schema(schema_hash, project = nil)
348
+ if ! project.nil?
349
+ if schema_hash.has_key?("id")
350
+ if ! schema_hash['id'].index(project)
351
+ schema_hash['id'] = "#{project}/#{schema_hash['id']}"
352
+ end
353
+ else
354
+ DataMapper.logger.error("You need an id key/value in the hash")
355
+ end
356
+ end
357
+
358
+ path = "/Class/#{schema_hash['id']}"
359
+ result = @persevere.delete(path)
360
+ if result.code == "204"
361
+ return true
362
+ else
363
+ return false
364
+ end
365
+ end
366
+
367
+ private
368
+
369
+ ##
370
+ # Make a new instance of the adapter. The @model_records ivar is
371
+ # the 'data-store' for this adapter. It is not shared amongst
372
+ # multiple incarnations of this adapter, eg
373
+ # DataMapper.setup(:default, :adapter => :in_memory);
374
+ # DataMapper.setup(:alternate, :adapter => :in_memory) do not
375
+ # share the data-store between them.
376
+ #
377
+ # @param [String, Symbol] name
378
+ # The name of the DataMapper::Repository using this adapter.
379
+ # @param [String, Hash] uri_or_options
380
+ # The connection uri string, or a hash of options to set up
381
+ # the adapter
382
+ #
383
+ # @api semipublic
384
+ def initialize(name, uri_or_options)
385
+ super
386
+
387
+ if uri_or_options.class
388
+ @identity_maps = {}
389
+ end
390
+
391
+ @options = Hash.new
392
+
393
+ uri_or_options.each do |k,v|
394
+ @options[k.to_sym] = v
395
+ end
396
+
397
+ @options[:scheme] = @options[:adapter]
398
+ @options.delete(:scheme)
399
+
400
+ # @resource_naming_convention = NamingConventions::Resource::Underscored
401
+ @resource_naming_convention = lambda do |value|
402
+ # value.split('::').map{ |val| Extlib::Inflection.underscore(val) }.join('__')
403
+ # Extlib::Inflection.underscore(value).gsub('/', '__')
404
+ ActiveSupport::Inflector.underscore(value).gsub('/', '__')
405
+ end
406
+
407
+ @identity_maps = {}
408
+ @persevere = nil
409
+ @prepped = false
410
+ @schema_backups = Array.new
411
+ @last_backup = nil
412
+
413
+ connect
414
+ end
415
+
416
+ private
417
+
418
+ ##
419
+ #
420
+ def connect
421
+ if ! @prepped
422
+ uri = URI::HTTP.build(@options).to_s
423
+ @persevere = PersevereClient.new(uri)
424
+ prep_persvr unless @prepped
425
+ end
426
+ end
427
+
428
+
429
+ def check_schemas
430
+ schemas = @persevere.retrieve("/Class").body
431
+ md5 = Digest::MD5.hexdigest(schemas)
432
+
433
+ if ! @last_backup.nil?
434
+ if @last_backup[:hash] != md5
435
+ DataMapper.logger.debug("Schemas changed, do you know why? (#{md5} :: #{@last_backup[:hash]})")
436
+ @schema_backups.each do |sb|
437
+ if sb[:hash] == md5
438
+ DataMapper.logger.debug("Schemas reverted to #{sb.inspect}")
439
+ end
440
+ end
441
+ end
442
+ end
443
+ end
444
+
445
+ def save_schemas
446
+ schemas = @persevere.retrieve("/Class").body
447
+ md5 = Digest::MD5.hexdigest(schemas)
448
+ @last_backup = { :hash => md5, :schemas => schemas, :timestamp => Time.now }
449
+ @schema_backups << @last_backup
450
+ # Dump to filesystem
451
+ end
452
+
453
+ def get_classes
454
+ # Because this is an AbstractAdapter and not a
455
+ # DataObjectAdapter, we can't assume there are any schemas
456
+ # present, so we retrieve the ones that exist and keep them up
457
+ # to date
458
+ classes = Array.new
459
+ result = @persevere.retrieve('/Class[=id]')
460
+ if result.code == "200"
461
+ hresult = JSON.parse(result.body)
462
+ hresult.each do |cname|
463
+ junk,name = cname.split("/")
464
+ classes << name
465
+ end
466
+ else
467
+ DataMapper.logger.error("Error retrieving existing tables: #{result}")
468
+ end
469
+ classes
470
+ end
471
+
472
+ ##
473
+ #
474
+ def prep_persvr
475
+ #
476
+ # If the user specified a versioned datastore load the versioning REST code
477
+ #
478
+ unless get_classes.include?("Versioned") && @options[:versioned]
479
+ versioned_class =<<-EOF
480
+ {
481
+ id: "Versioned",
482
+ prototype: {
483
+ getVersionMethod: function() {
484
+ return java.lang.Class.forName("org.persvr.data.Persistable").getMethod("getVersion");
485
+ },
486
+ isCurrentVersion: function() {
487
+ return this.getVersionMethod().invoke(this).isCurrent();
488
+ },
489
+ getVersionNumber: function() {
490
+ return this.getVersionMethod().invoke(this).getVersionNumber();
491
+ },
492
+ getPrevious: function() {
493
+ var prev = this.getVersionMethod().invoke(this).getPreviousVersion();
494
+ return prev;
495
+ },
496
+ getAllPrevious: function() {
497
+
498
+ var current = this;
499
+ var prev = current && current.getPrevious();
500
+
501
+ var versions = []
502
+ while(current && prev) {
503
+ versions.push(prev);
504
+ current = prev;
505
+ prev = current.getPrevious();
506
+ }
507
+
508
+ return versions;
509
+ },
510
+ "representation:application/json+versioned": {
511
+ quality: 0.2,
512
+ output: function(object) {
513
+ var previous = object.getAllPrevious();
514
+ response.setContentType("application/json+versioned");
515
+ response.getOutputStream().print(JSON.stringify({
516
+ version: object.getVersionNumber(),
517
+ current: object,
518
+ versions: previous
519
+ }));
520
+ }
521
+ }
522
+ }
523
+ }
524
+ EOF
525
+
526
+ response = @persevere.create('/Class/', versioned_class, { 'Content-Type' => 'application/javascript' } )
527
+
528
+ # Check the response, this needs to be more robust and raise
529
+ # exceptions when there's a problem
530
+ if response.code == "201"# good:
531
+ DataMapper.logger.info("Created versioned class.")
532
+ else
533
+ DataMapper.logger.info("Failed to create versioned class.")
534
+ end
535
+ end
536
+ end
537
+ end # class Adapter
538
+ end # module Persevere
539
+
540
+ DataMapper::Adapters::PersevereAdapter = DataMapper::Persevere::Adapter
541
+ DataMapper::Adapters.const_added(:PersevereAdapter)
542
+ end # DataMapper
543
+
544
+