vagas-orientdb4r 0.5.2

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