vagas-orientdb4r 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +26 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE +202 -0
  7. data/README.rdoc +124 -0
  8. data/Rakefile +43 -0
  9. data/changelog.txt +60 -0
  10. data/ci/initialize-ci.sh +36 -0
  11. data/fstudy/design_v1.dia +0 -0
  12. data/fstudy/design_v1.png +0 -0
  13. data/fstudy/domain_model.dia +0 -0
  14. data/fstudy/domain_model.png +0 -0
  15. data/fstudy/flat_class_perf.rb +56 -0
  16. data/fstudy/sample1_object_diagram.dia +0 -0
  17. data/fstudy/sample1_object_diagram.png +0 -0
  18. data/fstudy/study_case.rb +87 -0
  19. data/fstudy/technical_feasibility.rb +256 -0
  20. data/lib/orientdb4r.rb +115 -0
  21. data/lib/orientdb4r/bin/client.rb +86 -0
  22. data/lib/orientdb4r/bin/connection.rb +29 -0
  23. data/lib/orientdb4r/bin/constants.rb +20 -0
  24. data/lib/orientdb4r/bin/io.rb +38 -0
  25. data/lib/orientdb4r/bin/protocol28.rb +101 -0
  26. data/lib/orientdb4r/bin/protocol_factory.rb +25 -0
  27. data/lib/orientdb4r/chained_error.rb +37 -0
  28. data/lib/orientdb4r/client.rb +364 -0
  29. data/lib/orientdb4r/load_balancing.rb +113 -0
  30. data/lib/orientdb4r/node.rb +42 -0
  31. data/lib/orientdb4r/rest/client.rb +517 -0
  32. data/lib/orientdb4r/rest/excon_node.rb +115 -0
  33. data/lib/orientdb4r/rest/model.rb +159 -0
  34. data/lib/orientdb4r/rest/node.rb +43 -0
  35. data/lib/orientdb4r/rest/restclient_node.rb +77 -0
  36. data/lib/orientdb4r/rid.rb +54 -0
  37. data/lib/orientdb4r/utils.rb +203 -0
  38. data/lib/orientdb4r/version.rb +39 -0
  39. data/orientdb4r.gemspec +37 -0
  40. data/test/bin/test_client.rb +21 -0
  41. data/test/readme_sample.rb +38 -0
  42. data/test/test_client.rb +93 -0
  43. data/test/test_database.rb +261 -0
  44. data/test/test_ddo.rb +237 -0
  45. data/test/test_dmo.rb +115 -0
  46. data/test/test_document_crud.rb +184 -0
  47. data/test/test_gremlin.rb +52 -0
  48. data/test/test_helper.rb +10 -0
  49. data/test/test_loadbalancing.rb +81 -0
  50. data/test/test_utils.rb +67 -0
  51. metadata +136 -0
@@ -0,0 +1,42 @@
1
+ module Orientdb4r
2
+
3
+ ###
4
+ # This class represents a single sever/node
5
+ # in the Distributed Multi-Master Architecture.
6
+ class Node
7
+ include Utils
8
+
9
+ attr_reader :host, :port # they are immutable
10
+ attr_reader :session_id
11
+
12
+ ###
13
+ # Constructor.
14
+ def initialize(host, port)
15
+ raise ArgumentError, 'host cannot be blank' if blank? host
16
+ raise ArgumentError, 'port cannot be blank' if blank? port
17
+ @host = host
18
+ @port = port
19
+ end
20
+
21
+
22
+ ###
23
+ # Cleans up resources used by the node.
24
+ def cleanup
25
+ @session_id = nil
26
+ end
27
+
28
+
29
+ ###
30
+ # Gets URL of the remote node.
31
+ def url
32
+ raise NotImplementedError, 'this should be overridden by subclass'
33
+ end
34
+
35
+
36
+ def to_s #:nodoc:
37
+ "Node(host=#{host},port=#{port})"
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,517 @@
1
+ module Orientdb4r
2
+
3
+ class RestClient < Client
4
+ include Aop2
5
+
6
+
7
+ before [:query, :command, :gremlin, :batch], :assert_connected
8
+ before [:create_class, :get_class, :class_exists?, :drop_class, :create_property], :assert_connected
9
+ before [:create_document, :get_document, :update_document, :delete_document], :assert_connected
10
+ around [:query, :command], :time_around
11
+
12
+
13
+ def initialize(options) #:nodoc:
14
+ super()
15
+ options_pattern = {
16
+ :host => 'localhost', :port => 2480, :ssl => false,
17
+ :nodes => :optional,
18
+ :connection_library => :restclient,
19
+ # :connection_library => :excon,
20
+ :load_balancing => :sequence,
21
+ :proxy => :optional,
22
+ :user_agent => :optional,
23
+ :lb_recover_time => :optional
24
+ }
25
+ verify_and_sanitize_options(options, options_pattern)
26
+
27
+ # fake nodes for single server
28
+ if options[:nodes].nil?
29
+ options[:nodes] = [{:host => options[:host], :port => options[:port], :ssl => options[:ssl]}]
30
+ end
31
+ raise ArgumentError, 'nodes has to be array' unless options[:nodes].is_a? Array
32
+
33
+ # instantiate nodes according to HTTP library
34
+ @connection_library = options[:connection_library]
35
+ node_clazz = case connection_library
36
+ when :restclient then Orientdb4r::RestClientNode
37
+ when :excon then Orientdb4r::ExconNode
38
+ else raise ArgumentError, "unknown connection library: #{connection_library}"
39
+ end
40
+
41
+ # nodes
42
+ options[:nodes].each do |node_options|
43
+ verify_and_sanitize_options(node_options, options_pattern)
44
+ @nodes << node_clazz.new(node_options[:host], node_options[:port], node_options[:ssl])
45
+ end
46
+
47
+ # load balancing
48
+ @load_balancing = options[:load_balancing]
49
+ @lb_strategy = case load_balancing
50
+ when :sequence then Orientdb4r::Sequence.new nodes.size
51
+ when :round_robin then Orientdb4r::RoundRobin.new nodes.size
52
+ else raise ArgumentError, "unknow load balancing type: #{load_balancing}"
53
+ end
54
+ # recover time
55
+ recover_time = options[:lb_recover_time]
56
+ @lb_strategy.recover_time = recover_time.to_i unless recover_time.nil?
57
+
58
+
59
+ # proxy
60
+ @proxy = options[:proxy]
61
+ unless proxy.nil?
62
+ case connection_library
63
+ when :restclient then ::RestClient.proxy = proxy
64
+ when :excon then nodes.each { |node| node.proxy = proxy }
65
+ end
66
+ end
67
+
68
+ # user-agent
69
+ agent = options[:user_agent]
70
+ unless agent.nil?
71
+ nodes.each { |node| node.user_agent = agent }
72
+ end
73
+
74
+ Orientdb4r::logger.info "client initialized with #{@nodes.size} node(s) "
75
+ Orientdb4r::logger.info "connection_library=#{options[:connection_library]}, load_balancing=#{load_balancing}"
76
+ end
77
+
78
+
79
+ # --------------------------------------------------------------- CONNECTION
80
+
81
+ def connect(options) #:nodoc:
82
+ options_pattern = { :database => :mandatory, :user => :mandatory, :password => :mandatory }
83
+ verify_and_sanitize_options(options, options_pattern)
84
+
85
+ @database = options[:database]
86
+ @user = options[:user]
87
+ @password = options[:password]
88
+
89
+ @nodes.each { |node| node.cleanup } # destroy all used session <= problem in 1.3.0-SNAPSHOT
90
+ begin
91
+ http_response = call_server(:method => :get, :uri => "connect/#{@database}")
92
+ rescue
93
+ @connected = false
94
+ @user = nil
95
+ @password = nil
96
+ @database = nil
97
+ @nodes.each { |node| node.cleanup }
98
+ raise ConnectionError
99
+ end
100
+
101
+ rslt = process_response http_response
102
+ # no metadata in connect v1.4.0+
103
+ # https://groups.google.com/forum/?fromgroups=#!topic/orient-database/R0VoOfIyDng
104
+ #decorate_classes_with_model(rslt['classes']) unless rslt['classes'].nil?
105
+
106
+ if @connection_library == :excon
107
+ Orientdb4r::logger.info "successfully connected to server, code=#{http_response.code}"
108
+ else
109
+ Orientdb4r::logger.info "successfully connected to server, code=#{rslt.code}"
110
+ end
111
+
112
+ @connected = true
113
+ @connected
114
+ end
115
+
116
+
117
+ def disconnect #:nodoc:
118
+ return unless @connected
119
+
120
+ begin
121
+ call_server(:method => :get, :uri => 'disconnect')
122
+ # https://groups.google.com/forum/?fromgroups#!topic/orient-database/5MAMCvFavTc
123
+ # Disconnect doesn't require you're authenticated.
124
+ # It always returns 401 because some browsers intercept this and avoid to reuse the same session again.
125
+ ensure
126
+ @connected = false
127
+ @user = nil
128
+ @password = nil
129
+ @database = nil
130
+ @nodes.each { |node| node.cleanup }
131
+ Orientdb4r::logger.debug 'disconnected from server'
132
+ end
133
+ end
134
+
135
+
136
+ def server(options={}) #:nodoc:
137
+ options_pattern = { :user => :optional, :password => :optional }
138
+ verify_options(options, options_pattern)
139
+
140
+ params = { :method => :get, :uri => 'server' }
141
+ params[:no_session] = true # out of existing session which represents an already done authentication
142
+
143
+ # additional authentication allowed, overriden in 'call_server' if not defined
144
+ params[:user] = options[:user] if options.include? :user
145
+ params[:password] = options[:password] if options.include? :password
146
+
147
+ response = call_server params
148
+ process_response(response)
149
+ end
150
+
151
+
152
+ # ----------------------------------------------------------------- DATABASE
153
+
154
+ def create_database(options) #:nodoc:
155
+ options_pattern = {
156
+ :database => :mandatory, :storage => :memory, :type => :document,
157
+ :user => :optional, :password => :optional
158
+ }
159
+ verify_and_sanitize_options(options, options_pattern)
160
+ verify_options(options.select {|k,v| k === :storage or k === :type}, {:storage => [:memory, :plocal], :type => [:document, :graph]})
161
+
162
+ params = { :method => :post, :uri => "database/#{options[:database]}/#{options[:storage].to_s}/#{options[:type].to_s}" }
163
+ params[:no_session] = true # out of existing session which represents an already done authentication
164
+
165
+ # additional authentication allowed, overriden in 'call_server' if not defined
166
+ params[:user] = options[:user] if options.include? :user
167
+ params[:password] = options[:password] if options.include? :password
168
+
169
+ response = call_server params
170
+ process_response(response)
171
+ end
172
+
173
+
174
+ #> curl --user admin:admin http://localhost:2480/database/temp
175
+ def get_database(options=nil) #:nodoc:
176
+ raise ArgumentError, 'options have to be a Hash' if !options.nil? and !options.kind_of? Hash
177
+
178
+ if options.nil?
179
+ # use database from connect
180
+ raise ConnectionError, 'client has to be connected if no params' unless connected?
181
+ options = { :database => database }
182
+ end
183
+
184
+ options_pattern = { :database => :mandatory, :user => :optional, :password => :optional }
185
+ verify_options(options, options_pattern)
186
+
187
+ params = {:method => :get, :uri => "database/#{options[:database]}"}
188
+ params[:no_session] = true # out of existing session which represents an already done authentication
189
+
190
+ # additional authentication allowed, overriden in 'call_server' if not defined
191
+ params[:user] = options[:user] if options.include? :user
192
+ params[:password] = options[:password] if options.include? :password
193
+
194
+ response = call_server params
195
+
196
+ # NotFoundError cannot be raised - no way how to recognize from 401 bad auth
197
+ rslt = process_response response
198
+ decorate_classes_with_model(rslt['classes']) unless rslt['classes'].nil?
199
+ rslt
200
+ end
201
+
202
+
203
+ def delete_database(options) #:nodoc:
204
+ options_pattern = {
205
+ :database => :mandatory, :user => :optional, :password => :optional
206
+ }
207
+ verify_and_sanitize_options(options, options_pattern)
208
+
209
+ params = { :method => :delete, :uri => "database/#{options[:database]}" }
210
+ params[:no_session] = true # out of existing session which represents an already done authentication
211
+
212
+ # additional authentication allowed, overriden in 'call_server' if not defined
213
+ params[:user] = options[:user] if options.include? :user
214
+ params[:password] = options[:password] if options.include? :password
215
+
216
+ response = call_server params
217
+ process_response(response)
218
+ end
219
+
220
+
221
+ #> curl --user root:root http://localhost:2480/listDatabases
222
+ def list_databases(options=nil) #:nodoc:
223
+ verify_and_sanitize_options(options, { :user => :optional, :password => :optional })
224
+
225
+ params = { :method => :get, :uri => 'listDatabases', :no_session => true }
226
+ # additional authentication allowed, overriden in 'call_server' if not defined
227
+ params[:user] = options[:user] if options.include? :user
228
+ params[:password] = options[:password] if options.include? :password
229
+
230
+ response = call_server params
231
+ rslt = process_response(response)
232
+ rslt['databases']
233
+ end
234
+
235
+
236
+ def export(options=nil) #:nodoc:
237
+ raise ArgumentError, 'options have to be a Hash' if !options.nil? and !options.kind_of? Hash
238
+
239
+ if options.nil? or (!options.nil? and options[:database].nil?)
240
+ # use database from connect
241
+ raise ConnectionError, 'client has to be connected if no database given' unless connected?
242
+ options = {} if options.nil?
243
+ options[:database] = database
244
+ end
245
+
246
+ options_pattern = { :database => :mandatory, :user => :optional, :password => :optional, :file => :optional }
247
+ verify_options(options, options_pattern)
248
+
249
+ params = {:method => :get, :uri => "export/#{options[:database]}"}
250
+ params[:no_session] = true # out of existing session which represents an already done authentication
251
+
252
+ # additional authentication allowed, overriden in 'call_server' if not defined
253
+ params[:user] = options[:user] if options.include? :user
254
+ params[:password] = options[:password] if options.include? :password
255
+
256
+ response = call_server params
257
+ rslt = process_response(response)
258
+
259
+ filename = options[:file]
260
+ filename = response.headers[:content_disposition].split('filename=')[-1] if filename.nil?
261
+ File.open(filename, 'w') do |f|
262
+ f.write rslt
263
+ end
264
+
265
+ filename
266
+ end
267
+
268
+
269
+ def import(options=nil) #:nodoc:
270
+ raise NotImplementedError, 'not working via REST API, see here for more info: https://github.com/nuvolabase/orientdb/issues/1345'
271
+ # params = {:method => :post, :uri => 'import/'}
272
+ # response = call_server params
273
+ end
274
+
275
+
276
+ # ---------------------------------------------------------------------- SQL
277
+
278
+ def query(sql, options=nil) #:nodoc:
279
+ raise ArgumentError, 'query is blank' if blank? sql
280
+
281
+ options_pattern = { :limit => :optional }
282
+ verify_options(options, options_pattern) unless options.nil?
283
+
284
+ limit = ''
285
+ limit = "/#{options[:limit]}" if !options.nil? and options.include?(:limit)
286
+
287
+ response = call_server(:method => :get, :uri => "query/#{@database}/sql/#{CGI::escape(sql)}#{limit}")
288
+ entries = process_response(response) do
289
+ raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
290
+ end
291
+
292
+ rslt = entries['result']
293
+ # mixin all document entries (they have '@class' attribute)
294
+ rslt.each { |doc| doc.extend Orientdb4r::DocumentMetadata unless doc['@class'].nil? }
295
+ rslt
296
+ end
297
+
298
+ ###
299
+ # Executes a Gremlin command against the database.
300
+ def gremlin(gremlin)
301
+ raise ArgumentError, 'gremlin query is blank' if blank? gremlin
302
+ response = call_server(:method => :post, :uri => "command/#{@database}/gremlin/#{CGI::escape(gremlin)}")
303
+ entries = process_response(response) do
304
+ raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
305
+ end
306
+
307
+ rslt = entries['result']
308
+ # mixin all document entries (they have '@class' attribute)
309
+ rslt.each { |doc| doc.extend Orientdb4r::DocumentMetadata unless doc['@class'].nil? }
310
+ rslt
311
+ end
312
+
313
+ def command(sql) #:nodoc:
314
+ raise ArgumentError, 'command is blank' if blank? sql
315
+ response = call_server(:method => :post, :uri => "command/#{@database}/sql/#{CGI::escape(sql)}")
316
+ process_response(response)
317
+ end
318
+
319
+ ###
320
+ # Executes a batch of operations in a single call.
321
+ def batch(operations)
322
+ response = call_server(:method => :post, :uri => "batch/#{@database}", \
323
+ :content_type => 'application/json', :data => operations.to_json)
324
+ process_response(response)
325
+ end
326
+
327
+
328
+ # -------------------------------------------------------------------- CLASS
329
+
330
+ def get_class(name) #:nodoc:
331
+ raise ArgumentError, "class name is blank" if blank?(name)
332
+
333
+ response = call_server(:method => :get, :uri => "class/#{@database}/#{name}")
334
+ rslt = process_response(response)
335
+
336
+ classes = [rslt]
337
+ decorate_classes_with_model(classes)
338
+ clazz = classes[0]
339
+ clazz.extend Orientdb4r::HashExtension
340
+ clazz.extend Orientdb4r::OClass
341
+ unless clazz['properties'].nil? # there can be a class without properties
342
+ clazz.properties.each do |prop|
343
+ prop.extend Orientdb4r::HashExtension
344
+ prop.extend Orientdb4r::Property
345
+ end
346
+ end
347
+
348
+ clazz
349
+ rescue NotFoundError
350
+ raise NotFoundError, 'class not found'
351
+ end
352
+
353
+
354
+ # ----------------------------------------------------------------- DOCUMENT
355
+
356
+ def create_document(doc) #:nodoc:
357
+ http_response = call_server(:method => :post, :uri => "document/#{@database}", \
358
+ :content_type => 'application/json', :data => doc.to_json)
359
+ resp = process_response(http_response) do
360
+ raise DataError, 'validation problem' if http_response.body =~ /OValidationException/
361
+ end
362
+
363
+ resp = ::JSON.parse(http_response.body) # workaround: https://groups.google.com/forum/?fromgroups=#!topic/orient-database/UJGAXYpHDmo
364
+ resp.extend Orientdb4r::DocumentMetadata
365
+ resp
366
+ end
367
+
368
+
369
+ def get_document(rid) #:nodoc:
370
+ rid = Rid.new(rid) unless rid.is_a? Rid
371
+ response = call_server(:method => :get, :uri => "document/#{@database}/#{rid.unprefixed}")
372
+ rslt = process_response(response) do
373
+ raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
374
+ raise NotFoundError, 'record not found' if response.body =~ /Record with id .* was not found/ # why after delete?
375
+ end
376
+
377
+ rslt.extend Orientdb4r::DocumentMetadata
378
+ rslt
379
+ end
380
+
381
+
382
+ def update_document(doc) #:nodoc:
383
+ raise ArgumentError, 'document is nil' if doc.nil?
384
+ raise ArgumentError, 'document has no RID' if doc.doc_rid.nil?
385
+ raise ArgumentError, 'document has no version' if doc.doc_version.nil?
386
+
387
+ rid = doc.doc_rid
388
+ doc.delete '@rid' # will be not updated
389
+
390
+ response = call_server(:method => :put, :uri => "document/#{@database}/#{rid.unprefixed}", \
391
+ :content_type => 'application/json', :data => doc.to_json)
392
+ process_response(response) do
393
+ raise DataError, 'concurrent modification' if response.body =~ /OConcurrentModificationException/
394
+ raise DataError, 'validation problem' if response.body =~ /OValidationException/
395
+ end
396
+ # empty http response
397
+ end
398
+
399
+
400
+ def delete_document(rid) #:nodoc:
401
+ rid = Rid.new(rid) unless rid.is_a? Rid
402
+
403
+ response = call_server(:method => :delete, :uri => "document/#{@database}/#{rid.unprefixed}")
404
+ process_response(response) do
405
+ raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
406
+ end
407
+ # empty http response
408
+ end
409
+
410
+
411
+ # ------------------------------------------------------------------ Helpers
412
+
413
+ private
414
+
415
+ ####
416
+ # Processes a HTTP response.
417
+ def process_response(response)
418
+ raise ArgumentError, 'response is null' if response.nil?
419
+
420
+ if block_given?
421
+ yield
422
+ end
423
+
424
+
425
+ # return code
426
+ if 400 == response.code
427
+ raise InvalidRequestError, compose_error_message(response)
428
+ elsif 401 == response.code
429
+ raise UnauthorizedError, compose_error_message(response)
430
+ elsif 404 == response.code
431
+ raise NotFoundError, compose_error_message(response)
432
+ elsif 409 == response.code
433
+ raise StateConflictError, compose_error_message(response)
434
+ elsif 500 == response.code
435
+ raise ServerError, compose_error_message(response)
436
+ elsif 2 != (response.code / 100)
437
+ raise OrientdbError, "unexpected return code, code=#{response.code}, body=#{compose_error_message(response)}"
438
+ end
439
+
440
+ content_type = response.headers[:content_type] if connection_library == :restclient
441
+ content_type = response.headers['Content-Type'] if connection_library == :excon
442
+ content_type ||= 'text/plain'
443
+
444
+ rslt = case
445
+ when content_type.start_with?('text/plain')
446
+ response.body
447
+ when content_type.start_with?('application/x-gzip')
448
+ response.body
449
+ when content_type.start_with?('application/json')
450
+ ::JSON.parse(response.body)
451
+ else
452
+ raise OrientdbError, "unsuported content type: #{content_type}"
453
+ end
454
+
455
+ rslt
456
+ end
457
+
458
+
459
+ ###
460
+ # Composes message of an error raised if the HTTP response doesn't
461
+ # correspond with expectation.
462
+ def compose_error_message(http_response, max_len=200)
463
+ msg = http_response.body.gsub("\n", ' ')
464
+ if (matcher = msg.match(/"content": "([^"]+)"/))
465
+ msg = matcher[1]
466
+ end
467
+ msg = "#{msg[0..max_len]} ..." if msg.size > max_len
468
+ msg
469
+ end
470
+
471
+
472
+ # @deprecated
473
+ def process_restclient_response(response, options={})
474
+ raise ArgumentError, 'response is null' if response.nil?
475
+
476
+ # raise problem if other code than 200
477
+ if options[:mode] == :strict and 200 != response.code
478
+ raise OrientdbError, "unexpeted return code, code=#{response.code}"
479
+ end
480
+ # log warning if other than 200 and raise problem if other code than 'Successful 2xx'
481
+ if options[:mode] == :warning
482
+ if 200 != response.code and 2 == (response.code / 100)
483
+ Orientdb4r::logger.warn "expected return code 200, but received #{response.code}"
484
+ elseif 200 != response.code
485
+ raise OrientdbError, "unexpeted return code, code=#{response.code}"
486
+ end
487
+ end
488
+
489
+ content_type = response.headers[:content_type]
490
+ content_type ||= 'text/plain'
491
+
492
+ rslt = case
493
+ when content_type.start_with?('text/plain')
494
+ response.body
495
+ when content_type.start_with?('application/json')
496
+ ::JSON.parse(response.body)
497
+ end
498
+
499
+ rslt
500
+ end
501
+
502
+ def decorate_classes_with_model(classes)
503
+ classes.each do |clazz|
504
+ clazz.extend Orientdb4r::HashExtension
505
+ clazz.extend Orientdb4r::OClass
506
+ unless clazz['properties'].nil? # there can be a class without properties
507
+ clazz.properties.each do |prop|
508
+ prop.extend Orientdb4r::HashExtension
509
+ prop.extend Orientdb4r::Property
510
+ end
511
+ end
512
+ end
513
+ end
514
+
515
+ end
516
+
517
+ end