gotime_aws 2.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,989 @@
1
+ puts 'WARNING: ActiveSdb is deprecated, please use SimpleRecord at https://github.com/appoxy/simple_record/ instead.'
2
+
3
+ # Copyright (c) 2008 RightScale Inc
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #
24
+
25
+ begin
26
+ require 'uuidtools'
27
+ rescue LoadError => e
28
+ STDERR.puts("RightSDB requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
29
+ exit
30
+ end
31
+
32
+ module Aws
33
+
34
+ # = Aws::ActiveSdb -- RightScale SDB interface (alpha release)
35
+ # The Aws::ActiveSdb class provides a complete interface to Amazon's Simple
36
+ # Database Service.
37
+ #
38
+ # ActiveSdb is in alpha and does not load by default with the rest of Aws. You must use an additional require statement to load the ActiveSdb class. For example:
39
+ #
40
+ # require 'right_aws'
41
+ # require 'sdb/active_sdb'
42
+ #
43
+ # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a
44
+ # dependency of Aws.
45
+ #
46
+ # Simple ActiveSdb usage example:
47
+ #
48
+ # class Client < Aws::ActiveSdb::Base
49
+ # end
50
+ #
51
+ # # connect to SDB
52
+ # Aws::ActiveSdb.establish_connection
53
+ #
54
+ # # create domain
55
+ # Client.create_domain
56
+ #
57
+ # # create initial DB
58
+ # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
59
+ # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
60
+ # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
61
+ # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
62
+ # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
63
+ # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
64
+ #
65
+ # # find all Bushes in USA
66
+ # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
67
+ # client.reload
68
+ # puts client.attributes.inspect
69
+ # end
70
+ #
71
+ # # find all Maries through the world
72
+ # Client.find_all_by_name_and_gender('Mary','female').each do |mary|
73
+ # mary.reload
74
+ # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
75
+ # end
76
+ #
77
+ # # find new russian president
78
+ # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
79
+ # if medvedev
80
+ # medvedev.reload
81
+ # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
82
+ # end
83
+ #
84
+ # # retire old president
85
+ # Client.find_by_name('Putin').delete
86
+ #
87
+ # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
88
+ # sandy = Client.find(sandy_id)
89
+ # sandy.reload
90
+ # sandy.delete_values('hobby' => ['cooking'] )
91
+ # sandy.delete_attributes('country', 'gender')
92
+ #
93
+ # # remove domain
94
+ # Client.delete_domain
95
+ #
96
+ class ActiveSdb
97
+
98
+ module ActiveSdbConnect
99
+ def connection
100
+ @connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
101
+ end
102
+
103
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
104
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
105
+ # The +params+ are passed through as-is to Aws::SdbInterface.new
106
+ # Params:
107
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
108
+ # :port => 443 # Amazon service port: 80 or 443(default)
109
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
110
+ # :signature_version => '2' # The signature version : '0', '1' or '2' (default)
111
+ # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
112
+ # :connection_mode => :default # options are :default (will use best known option, may change in the future)
113
+ # :per_request (opens and closes a connection on every request to SDB)
114
+ # :single (same as old multi_thread=>false)
115
+ # :per_thread (same as old multi_thread=>true)
116
+ # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
117
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
118
+ # :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
119
+ # :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
120
+
121
+ def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
122
+ @connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
123
+ end
124
+
125
+ def close_connection
126
+ @connection.close_connection unless @connection.nil?
127
+ end
128
+ end
129
+
130
+ class ActiveSdbError < RuntimeError
131
+ end
132
+
133
+ class << self
134
+ include ActiveSdbConnect
135
+
136
+ # Retreive a list of domains.
137
+ #
138
+ # put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
139
+ #
140
+ def domains
141
+ connection.list_domains[:domains]
142
+ end
143
+
144
+ # Create new domain.
145
+ # Raises no errors if the domain already exists.
146
+ #
147
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
148
+ #
149
+ def create_domain(domain_name)
150
+ connection.create_domain(domain_name)
151
+ end
152
+
153
+ # Remove domain from SDB.
154
+ # Raises no errors if the domain does not exist.
155
+ #
156
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
157
+ #
158
+ def delete_domain(domain_name)
159
+ connection.delete_domain(domain_name)
160
+ end
161
+ end
162
+
163
+ class Base
164
+
165
+ class << self
166
+ include ActiveSdbConnect
167
+
168
+ # next_token value returned by last find: is useful to continue finding
169
+ attr_accessor :next_token
170
+
171
+ # Returns a Aws::SdbInterface object
172
+ #
173
+ # class A < Aws::ActiveSdb::Base
174
+ # end
175
+ #
176
+ # class B < Aws::ActiveSdb::Base
177
+ # end
178
+ #
179
+ # class C < Aws::ActiveSdb::Base
180
+ # end
181
+ #
182
+ # Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
183
+ #
184
+ # C.establish_connection 'key_id_2', 'secret_key_2'
185
+ #
186
+ # # A and B uses the default connection, C - uses its own
187
+ # puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
188
+ # puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
189
+ # puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
190
+ #
191
+ def connection
192
+ @connection || ActiveSdb::connection
193
+ end
194
+
195
+ @domain = nil
196
+
197
+ # Current domain name.
198
+ #
199
+ # # if 'ActiveSupport' is not loaded then class name converted to downcase
200
+ # class Client < Aws::ActiveSdb::Base
201
+ # end
202
+ # puts Client.domain #=> 'client'
203
+ #
204
+ # # if 'ActiveSupport' is loaded then class name being tableized
205
+ # require 'activesupport'
206
+ # class Client < Aws::ActiveSdb::Base
207
+ # end
208
+ # puts Client.domain #=> 'clients'
209
+ #
210
+ # # Explicit domain name definition
211
+ # class Client < Aws::ActiveSdb::Base
212
+ # set_domain_name :foreign_clients
213
+ # end
214
+ # puts Client.domain #=> 'foreign_clients'
215
+ #
216
+ def domain
217
+ unless @domain
218
+ if defined? ActiveSupport::CoreExtensions::String::Inflections
219
+ @domain = name.tableize
220
+ else
221
+ @domain = name.downcase
222
+ end
223
+ end
224
+ @domain
225
+ end
226
+
227
+ # Change the default domain name to user defined.
228
+ #
229
+ # class Client < Aws::ActiveSdb::Base
230
+ # set_domain_name :foreign_clients
231
+ # end
232
+ #
233
+ def set_domain_name(domain)
234
+ @domain = domain.to_s
235
+ end
236
+
237
+ # Create domain at SDB.
238
+ # Raises no errors if the domain already exists.
239
+ #
240
+ # class Client < Aws::ActiveSdb::Base
241
+ # end
242
+ # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
243
+ #
244
+ def create_domain
245
+ connection.create_domain(domain)
246
+ end
247
+
248
+ # Remove domain from SDB.
249
+ # Raises no errors if the domain does not exist.
250
+ #
251
+ # class Client < Aws::ActiveSdb::Base
252
+ # end
253
+ # Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
254
+ #
255
+ def delete_domain
256
+ connection.delete_domain(domain)
257
+ end
258
+
259
+ #
260
+ # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
261
+ #
262
+ def find(*args)
263
+ options = args.last.is_a?(Hash) ? args.pop : {}
264
+ case args.first
265
+ when nil then
266
+ raise "Invalid parameters passed to find: nil."
267
+ when :all then
268
+ sql_select(options)[:items]
269
+ when :first then
270
+ sql_select(options.merge(:limit => 1))[:items].first
271
+ when :count then
272
+ res = sql_select(options.merge(:count => true))[:count]
273
+ res
274
+ else
275
+ res = select_from_ids(args, options)
276
+ return res[:single] if res[:single]
277
+ return res[:items]
278
+ end
279
+ end
280
+
281
+ #
282
+ # Same as find, but will return SimpleDB metadata like :request_id and :box_usage
283
+ #
284
+ def find_with_metadata(*args)
285
+ options = args.last.is_a?(Hash) ? args.pop : {}
286
+ case args.first
287
+ when nil then
288
+ raise "Invalid parameters passed to find: nil."
289
+ when :all then
290
+ sql_select(options)
291
+ when :first then
292
+ sql_select(options.merge(:limit => 1))
293
+ when :count then
294
+ res = sql_select(options.merge(:count => true))
295
+ res
296
+ else
297
+ select_from_ids args, options
298
+ end
299
+ end
300
+
301
+ # Perform a SQL-like select request.
302
+ #
303
+ # Single record:
304
+ #
305
+ # Client.select(:first)
306
+ # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
307
+ # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
308
+ #
309
+ # Bunch of records:
310
+ #
311
+ # Client.select(:all)
312
+ # Client.select(:all, :limit => 10)
313
+ # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
314
+ # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
315
+ #
316
+ # Records by ids:
317
+ #
318
+ # Client.select('1')
319
+ # Client.select('1234987b4583475347523948')
320
+ # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
321
+ #
322
+ # Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
323
+ #
324
+ # Client.select_by_name('Matias Rust')
325
+ # Client.select_by_name_and_city('Putin','Moscow')
326
+ # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
327
+ #
328
+ # Client.select_all_by_author('G.Bush jr')
329
+ # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
330
+ # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
331
+ #
332
+ # Continue listing:
333
+ #
334
+ # # initial listing
335
+ # Client.select(:all, :limit => 10)
336
+ # # continue listing
337
+ # begin
338
+ # Client.select(:all, :limit => 10, :next_token => Client.next_token)
339
+ # end while Client.next_token
340
+ #
341
+ # Sort oder:
342
+ # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
343
+ #
344
+ # Client.select(:all) # returns all records
345
+ # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
346
+ # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
347
+ #
348
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
349
+ #
350
+ def select(*args)
351
+ find(*args)
352
+ end
353
+
354
+ def generate_id # :nodoc:
355
+ UUIDTools::UUID.timestamp_create().to_s
356
+ end
357
+
358
+ protected
359
+
360
+ # Select
361
+
362
+ def select_from_ids(args, options) # :nodoc:
363
+ cond = []
364
+ # detect amount of records requested
365
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
366
+ # flatten ids
367
+ args = args.to_a.flatten
368
+ args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
369
+ ids_cond = "(#{cond.join(' OR ')})"
370
+ # user defined :conditions to string (if it was defined)
371
+ options[:conditions] = build_conditions(options[:conditions])
372
+ # join ids condition and user defined conditions
373
+ options[:conditions] = Aws::Utils.blank?(options[:conditions]) ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
374
+ #puts 'options=' + options.inspect
375
+ result = sql_select(options)
376
+ #puts 'select_from_ids result=' + result.inspect
377
+ # if one record was requested then return it
378
+ unless bunch_of_records_requested
379
+ record = result[:items].first
380
+ # railse if nothing was found
381
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
382
+ result[:single] = record
383
+ else
384
+ # if a bunch of records was requested then return check that we found all of them
385
+ # and return as an array
386
+ unless args.size == result[:items].size
387
+ # todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
388
+ id_list = args.map{|i| "'#{i}'"}.join(',')
389
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
390
+ else
391
+ result
392
+ end
393
+ end
394
+ result
395
+ end
396
+
397
+ def sql_select(options) # :nodoc:
398
+ count = options[:count] || false
399
+ #puts 'count? ' + count.to_s
400
+ @next_token = options[:next_token]
401
+ @consistent_read = options[:consistent_read]
402
+ select_expression = build_select(options)
403
+ # request items
404
+ query_result = self.connection.select(select_expression, @next_token, @consistent_read)
405
+ # puts 'QR=' + query_result.inspect
406
+ ret = {}
407
+ if count
408
+ ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
409
+ ret.merge!(query_result)
410
+ return ret
411
+ end
412
+ @next_token = query_result[:next_token]
413
+ items = query_result.delete(:items).map do |hash|
414
+ id, attributes = hash.shift
415
+ new_item = self.new( )
416
+ new_item.initialize_from_db(attributes.merge({ 'id' => id }))
417
+ new_item.mark_as_old
418
+ new_item
419
+ end
420
+ ret[:items] = items
421
+ ret.merge!(query_result)
422
+ ret
423
+ end
424
+
425
+ # select_by helpers
426
+ def select_all_by_(format_str, args, options) # :nodoc:
427
+ fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
428
+ conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
429
+ options[:conditions] = [conditions, *args]
430
+ find(:all, options)
431
+ end
432
+
433
+ def select_by_(format_str, args, options) # :nodoc:
434
+ options[:limit] = 1
435
+ select_all_by_(format_str, args, options).first
436
+ end
437
+
438
+ # Query
439
+
440
+ # Returns an array of query attributes.
441
+ # Query_expression must be a well formated SDB query string:
442
+ # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
443
+ def query_attributes(query_expression) # :nodoc:
444
+ attrs = []
445
+ array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
446
+ until array.empty? do
447
+ attrs << array.shift # skip it's value
448
+ array.shift #
449
+ end
450
+ attrs
451
+ end
452
+
453
+ # Returns an array of [attribute_name, 'asc'|'desc']
454
+ def sort_options(sort_string)
455
+ sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
456
+ [$1, ($2 || 'asc')]
457
+ end
458
+
459
+ # Perform a query request.
460
+ #
461
+ # Options
462
+ # :query_expression nil | string | array
463
+ # :max_number_of_items nil | integer
464
+ # :next_token nil | string
465
+ # :sort_option nil | string "name desc|asc"
466
+ #
467
+ def query(options) # :nodoc:
468
+ @next_token = options[:next_token]
469
+ @consistent_read = options[:consistent_read]
470
+ query_expression = build_conditions(options[:query_expression])
471
+ # add sort_options to the query_expression
472
+ if options[:sort_option]
473
+ sort_by, sort_order = sort_options(options[:sort_option])
474
+ sort_query_expression = "['#{sort_by}' starts-with '']"
475
+ sort_by_expression = " sort '#{sort_by}' #{sort_order}"
476
+ # make query_expression to be a string (it may be null)
477
+ query_expression = query_expression.to_s
478
+ # quote from Amazon:
479
+ # The sort attribute must be present in at least one of the predicates of the query expression.
480
+ if Aws::Utils.blank?(query_expression)
481
+ query_expression = sort_query_expression
482
+ elsif !query_attributes(query_expression).include?(sort_by)
483
+ query_expression += " intersection #{sort_query_expression}"
484
+ end
485
+ query_expression += sort_by_expression
486
+ end
487
+ # request items
488
+ query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read)
489
+ @next_token = query_result[:next_token]
490
+ items = query_result[:items].map do |name|
491
+ new_item = self.new('id' => name)
492
+ new_item.mark_as_old
493
+ reload_if_exists(record) if options[:auto_load]
494
+ new_item
495
+ end
496
+ items
497
+ end
498
+
499
+ # reload a record unless it is nil
500
+ def reload_if_exists(record) # :nodoc:
501
+ record && record.reload
502
+ end
503
+
504
+ def reload_all_records(*list) # :nodoc:
505
+ list.flatten.each { |record| reload_if_exists(record) }
506
+ end
507
+
508
+ def find_every(options) # :nodoc:
509
+ records = query( :query_expression => options[:conditions],
510
+ :max_number_of_items => options[:limit],
511
+ :next_token => options[:next_token],
512
+ :sort_option => options[:sort] || options[:order],
513
+ :consistent_read => options[:consistent_read] )
514
+ options[:auto_load] ? reload_all_records(records) : records
515
+ end
516
+
517
+ def find_initial(options) # :nodoc:
518
+ options[:limit] = 1
519
+ record = find_every(options).first
520
+ options[:auto_load] ? reload_all_records(record).first : record
521
+ end
522
+
523
+ def find_from_ids(args, options) # :nodoc:
524
+ cond = []
525
+ # detect amount of records requested
526
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
527
+ # flatten ids
528
+ args = args.to_a.flatten
529
+ args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
530
+ ids_cond = "[#{cond.join(' OR ')}]"
531
+ # user defined :conditions to string (if it was defined)
532
+ options[:conditions] = build_conditions(options[:conditions])
533
+ # join ids condition and user defined conditions
534
+ options[:conditions] = Aws::Utils.blank?(options[:conditions]) ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
535
+ result = find_every(options)
536
+ # if one record was requested then return it
537
+ unless bunch_of_records_requested
538
+ record = result.first
539
+ # railse if nothing was found
540
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
541
+ options[:auto_load] ? reload_all_records(record).first : record
542
+ else
543
+ # if a bunch of records was requested then return check that we found all of them
544
+ # and return as an array
545
+ unless args.size == result.size
546
+ id_list = args.map{|i| "'#{i}'"}.join(',')
547
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
548
+ else
549
+ options[:auto_load] ? reload_all_records(result) : result
550
+ end
551
+ end
552
+ end
553
+
554
+ # find_by helpers
555
+ def find_all_by_(format_str, args, options) # :nodoc:
556
+ fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
557
+ conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
558
+ options[:conditions] = [conditions, *args]
559
+ find(:all, options)
560
+ end
561
+
562
+ def find_by_(format_str, args, options) # :nodoc:
563
+ options[:limit] = 1
564
+ find_all_by_(format_str, args, options).first
565
+ end
566
+
567
+ # Misc
568
+
569
+ def method_missing(method, *args) # :nodoc:
570
+ if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
571
+ # get rid of the find ones, only select now
572
+ to_send_to = $1
573
+ attributes = method.to_s[$1.length..method.to_s.length]
574
+ # puts 'attributes=' + attributes
575
+ if to_send_to[0...4] == "find"
576
+ to_send_to = "select" + to_send_to[4..to_send_to.length]
577
+ # puts 'CONVERTED ' + $1 + " to " + to_send_to
578
+ end
579
+
580
+ options = args.last.is_a?(Hash) ? args.pop : {}
581
+ __send__(to_send_to, attributes, args, options)
582
+ else
583
+ super(method, *args)
584
+ end
585
+ end
586
+
587
+ def build_select(options) # :nodoc:
588
+ select = options[:select] || '*'
589
+ select = options[:count] ? "count(*)" : select
590
+ #puts 'select=' + select.to_s
591
+ from = options[:from] || domain
592
+ conditions = options[:conditions] ? " WHERE #{build_conditions(options[:conditions])}" : ''
593
+ order = options[:order] ? " ORDER BY #{options[:order]}" : ''
594
+ limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
595
+ # mix sort by argument (it must present in response)
596
+ unless Aws::Utils.blank?(order)
597
+ sort_by, sort_order = sort_options(options[:order])
598
+ conditions << (Aws::Utils.blank?(conditions) ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
599
+ end
600
+ "SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
601
+ end
602
+
603
+ def build_conditions(conditions) # :nodoc:
604
+ case
605
+ when conditions.is_a?(Array) then
606
+ connection.query_expression_from_array(conditions)
607
+ when conditions.is_a?(Hash) then
608
+ connection.query_expression_from_hash(conditions)
609
+ else
610
+ conditions
611
+ end
612
+ end
613
+
614
+ end
615
+
616
+ public
617
+
618
+ # instance attributes
619
+ attr_accessor :attributes
620
+
621
+ # item name
622
+ attr_accessor :id
623
+
624
+ # Create new Item instance.
625
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
626
+ #
627
+ # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
628
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
629
+ # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
630
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
631
+ #
632
+ def initialize(attrs={})
633
+ @attributes = uniq_values(attrs)
634
+ @new_record = true
635
+ end
636
+
637
+ # This is to separate initialization from user vs coming from db (ie: find())
638
+ def initialize_from_db(attrs={})
639
+ initialize(attrs)
640
+ end
641
+
642
+ # Create and save new Item instance.
643
+ # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
644
+ #
645
+ # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
646
+ # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
647
+ #
648
+ def self.create(attributes={})
649
+ item = self.new(attributes)
650
+ item.save
651
+ item
652
+ end
653
+
654
+ # Returns an item id. Same as: item['id'] or item.attributes['id']
655
+ def id
656
+ @attributes['id']
657
+ end
658
+
659
+ # Sets an item id.
660
+ def id=(id)
661
+ @attributes['id'] = id.to_s
662
+ end
663
+
664
+ # Returns a hash of all the attributes.
665
+ #
666
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
667
+ #
668
+ def attributes
669
+ @attributes.dup
670
+ end
671
+
672
+ # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
673
+ # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
674
+ #
675
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
676
+ # # set new attributes ('id' is missed)
677
+ # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
678
+ # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
679
+ # # set new attributes ('id' is set)
680
+ # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
681
+ # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
682
+ #
683
+ def attributes=(attrs)
684
+ old_id = @attributes['id']
685
+ @attributes = uniq_values(attrs)
686
+ if Aws::Utils.blank?(@attributes['id']) && !Aws::Utils.blank?(old_id)
687
+ @attributes['id'] = old_id
688
+ end
689
+ self.attributes
690
+ end
691
+
692
+ def connection
693
+ self.class.connection
694
+ end
695
+
696
+ # Item domain name.
697
+ def domain
698
+ self.class.domain
699
+ end
700
+
701
+ # Returns the values of the attribute identified by +attribute+.
702
+ #
703
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
704
+ #
705
+ def [](attribute)
706
+ @attributes[attribute.to_s]
707
+ end
708
+
709
+ # Updates the attribute identified by +attribute+ with the specified +values+.
710
+ #
711
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
712
+ # item['Cat'] = ["Whiskas", "chicken"]
713
+ # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
714
+ #
715
+ def []=(attribute, values)
716
+ attribute = attribute.to_s
717
+ @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
718
+
719
+ end
720
+
721
+ # Reload attributes from SDB. Replaces in-memory attributes.
722
+ #
723
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
724
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
725
+ #
726
+ def reload
727
+ raise_on_id_absence
728
+ old_id = id
729
+ attrs = connection.get_attributes(domain, id)[:attributes]
730
+ @attributes = {}
731
+ unless attrs.nil? || attrs.empty?
732
+ attrs.each { |attribute, values| @attributes[attribute] = values }
733
+ @attributes['id'] = old_id
734
+ end
735
+ mark_as_old
736
+ @attributes
737
+ end
738
+
739
+ # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
740
+ # +attrs_list+ is an array or comma separated list of attributes names.
741
+ # Returns a hash of loaded attributes.
742
+ #
743
+ # This is not the best method to get a bunch of attributes because
744
+ # a web service call is being performed for every attribute.
745
+ #
746
+ # item = Client.find_by_name('Cat')
747
+ # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
748
+ #
749
+ def reload_attributes(*attrs_list)
750
+ raise_on_id_absence
751
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
752
+ attrs_list.delete('id')
753
+ result = {}
754
+ attrs_list.flatten.uniq.each do |attribute|
755
+ attribute = attribute.to_s
756
+ values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
757
+ unless Aws::Utils.blank?(values)
758
+ @attributes[attribute] = result[attribute] = values
759
+ else
760
+ @attributes.delete(attribute)
761
+ end
762
+ end
763
+ mark_as_old
764
+ result
765
+ end
766
+
767
+ # Stores in-memory attributes to SDB.
768
+ # Adds the attributes values to already stored at SDB.
769
+ # Returns a hash of stored attributes.
770
+ #
771
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
772
+ # sandy['toys'] = 'boys'
773
+ # sandy.put
774
+ # sandy['toys'] = 'patchwork'
775
+ # sandy.put
776
+ # sandy['toys'] = 'kids'
777
+ # sandy.put
778
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
779
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
780
+ #
781
+ # compare to +save+ method
782
+ def put
783
+ @attributes = uniq_values(@attributes)
784
+ prepare_for_update
785
+ attrs = @attributes.dup
786
+ attrs.delete('id')
787
+ connection.put_attributes(domain, id, attrs) unless Aws::Utils.blank?(attrs)
788
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
789
+ mark_as_old
790
+ @attributes
791
+ end
792
+
793
+ # Stores specified attributes.
794
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
795
+ # Returns a hash of saved attributes.
796
+ #
797
+ # see to +put+ method
798
+ def put_attributes(attrs)
799
+ attrs = uniq_values(attrs)
800
+ prepare_for_update
801
+ # if 'id' is present in attrs hash:
802
+ # replace internal 'id' attribute and remove it from the attributes to be sent
803
+ @attributes['id'] = attrs['id'] unless Aws::Utils.blank?(attrs['id'])
804
+ attrs.delete('id')
805
+ # add new values to all attributes from list
806
+ connection.put_attributes(domain, id, attrs) unless Aws::Utils.blank?(attrs)
807
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
808
+ attrs.each do |attribute, values|
809
+ @attributes[attribute] ||= []
810
+ @attributes[attribute] += values
811
+ @attributes[attribute].uniq!
812
+ end
813
+ mark_as_old
814
+ attributes
815
+ end
816
+
817
+ # Store in-memory attributes to SDB.
818
+ # Replaces the attributes values already stored at SDB by in-memory data.
819
+ # Returns a hash of stored attributes.
820
+ #
821
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
822
+ # sandy['toys'] = 'boys'
823
+ # sandy.save
824
+ # sandy['toys'] = 'patchwork'
825
+ # sandy.save
826
+ # sandy['toys'] = 'kids'
827
+ # sandy.save
828
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
829
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
830
+ #
831
+ # Options:
832
+ # - :except => Array of attributes to NOT save
833
+ #
834
+ # compare to +put+ method
835
+ def save(options={})
836
+ pre_save2
837
+ atts_to_save = @attributes.dup
838
+ #puts 'atts_to_save=' + atts_to_save.inspect
839
+ #options = params.first.is_a?(Hash) ? params.pop : {}
840
+ if options[:except]
841
+ options[:except].each do |e|
842
+ atts_to_save.delete(e).inspect
843
+ end
844
+ end
845
+ if options[:dirty] # Only used in simple_record right now
846
+ # only save if the attribute is dirty
847
+ dirty_atts = options[:dirty_atts]
848
+ atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
849
+ end
850
+ #puts 'atts_to_save2=' + atts_to_save.inspect
851
+ connection.put_attributes(domain, id, atts_to_save, :replace)
852
+ apres_save2
853
+ @attributes
854
+ end
855
+
856
+ def pre_save2
857
+ @attributes = uniq_values(@attributes)
858
+ prepare_for_update
859
+ end
860
+
861
+ def apres_save2
862
+ mark_as_old
863
+ end
864
+
865
+ # Replaces the attributes at SDB by the given values.
866
+ # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
867
+ # The other in-memory attributes are not being saved.
868
+ # Returns a hash of stored attributes.
869
+ #
870
+ # see +save+ method
871
+ def save_attributes(attrs)
872
+ prepare_for_update
873
+ attrs = uniq_values(attrs)
874
+ # if 'id' is present in attrs hash then replace internal 'id' attribute
875
+ unless Aws::Utils.blank?(attrs['id'])
876
+ @attributes['id'] = attrs['id']
877
+ else
878
+ attrs['id'] = id
879
+ end
880
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
881
+ attrs.each { |attribute, values| attrs[attribute] = values }
882
+ mark_as_old
883
+ attrs
884
+ end
885
+
886
+ # Remove specified values from corresponding attributes.
887
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
888
+ #
889
+ # sandy = Client.find_by_name 'Sandy'
890
+ # sandy.reload
891
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
892
+ # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
893
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
894
+ #
895
+ def delete_values(attrs)
896
+ raise_on_id_absence
897
+ attrs = uniq_values(attrs)
898
+ attrs.delete('id')
899
+ unless attrs.nil? || attrs.empty?
900
+ connection.delete_attributes(domain, id, attrs)
901
+ attrs.each do |attribute, values|
902
+ # remove the values from the attribute
903
+ if @attributes[attribute]
904
+ @attributes[attribute] -= values
905
+ else
906
+ # if the attribute is unknown remove it from a resulting list of fixed attributes
907
+ attrs.delete(attribute)
908
+ end
909
+ end
910
+ end
911
+ attrs
912
+ end
913
+
914
+ # Removes specified attributes from the item.
915
+ # +attrs_list+ is an array or comma separated list of attributes names.
916
+ # Returns the list of deleted attributes.
917
+ #
918
+ # sandy = Client.find_by_name 'Sandy'
919
+ # sandy.reload
920
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
921
+ # puts sandy.delete_attributes('toys') #=> ['toys']
922
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
923
+ #
924
+ def delete_attributes(*attrs_list)
925
+ raise_on_id_absence
926
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
927
+ attrs_list.delete('id')
928
+ unless attrs_list.empty?
929
+ connection.delete_attributes(domain, id, attrs_list)
930
+ attrs_list.each { |attribute| @attributes.delete(attribute) }
931
+ end
932
+ attrs_list
933
+ end
934
+
935
+ # Delete the Item entirely from SDB.
936
+ #
937
+ # sandy = Client.find_by_name 'Sandy'
938
+ # sandy.reload
939
+ # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
940
+ # puts sandy.delete
941
+ # sandy.reload
942
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
943
+ #
944
+ def delete
945
+ raise_on_id_absence
946
+ connection.delete_attributes(domain, id)
947
+ end
948
+
949
+ # Item ID
950
+ def to_s
951
+ @id
952
+ end
953
+
954
+ # Returns true if this object hasn‘t been saved yet.
955
+ def new_record?
956
+ @new_record
957
+ end
958
+
959
+ def mark_as_old # :nodoc:
960
+ @new_record = false
961
+ end
962
+
963
+ private
964
+
965
+ def raise_on_id_absence
966
+ raise ActiveSdbError.new('Unknown record id') unless id
967
+ end
968
+
969
+ def prepare_for_update
970
+ @attributes['id'] = self.class.generate_id if Aws::Utils.blank?(@attributes['id'])
971
+ end
972
+
973
+ def uniq_values(attributes=nil) # :nodoc:
974
+ attrs = {}
975
+ attributes.each do |attribute, values|
976
+ attribute = attribute.to_s
977
+ newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
978
+ attrs[attribute] = newval
979
+ if Aws::Utils.blank?(newval)
980
+ # puts "VALUE IS BLANK " + attribute.to_s + " val=" + values.inspect
981
+ attrs.delete(attribute)
982
+ end
983
+ end
984
+ attrs
985
+ end
986
+
987
+ end
988
+ end
989
+ end