neo4j-core 3.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +22 -0
  3. data/README.md +332 -0
  4. data/lib/neo4j-core.rb +27 -0
  5. data/lib/neo4j-core/cypher_translator.rb +34 -0
  6. data/lib/neo4j-core/hash_with_indifferent_access.rb +165 -0
  7. data/lib/neo4j-core/helpers.rb +25 -0
  8. data/lib/neo4j-core/label.rb +8 -0
  9. data/lib/neo4j-core/version.rb +5 -0
  10. data/lib/neo4j-embedded.rb +18 -0
  11. data/lib/neo4j-embedded/embedded_database.rb +29 -0
  12. data/lib/neo4j-embedded/embedded_label.rb +80 -0
  13. data/lib/neo4j-embedded/embedded_node.rb +163 -0
  14. data/lib/neo4j-embedded/embedded_relationship.rb +44 -0
  15. data/lib/neo4j-embedded/embedded_session.rb +151 -0
  16. data/lib/neo4j-embedded/property.rb +43 -0
  17. data/lib/neo4j-embedded/to_java.rb +49 -0
  18. data/lib/neo4j-server.rb +10 -0
  19. data/lib/neo4j-server/cypher_label.rb +28 -0
  20. data/lib/neo4j-server/cypher_node.rb +140 -0
  21. data/lib/neo4j-server/cypher_node_uncommited.rb +12 -0
  22. data/lib/neo4j-server/cypher_relationship.rb +82 -0
  23. data/lib/neo4j-server/cypher_response.rb +113 -0
  24. data/lib/neo4j-server/cypher_session.rb +156 -0
  25. data/lib/neo4j-server/cypher_transaction.rb +81 -0
  26. data/lib/neo4j-server/resource.rb +73 -0
  27. data/lib/neo4j/entity_equality.rb +9 -0
  28. data/lib/neo4j/jars/concurrentlinkedhashmap-lru-1.3.1.jar +0 -0
  29. data/lib/neo4j/jars/geronimo-jta_1.1_spec-1.1.1.jar +0 -0
  30. data/lib/neo4j/jars/lucene-core-3.6.2.jar +0 -0
  31. data/lib/neo4j/jars/neo4j-cypher-2.0.0-M06.jar +0 -0
  32. data/lib/neo4j/jars/neo4j-kernel-2.0-SNAPSHOT-tests.jar +0 -0
  33. data/lib/neo4j/jars/neo4j-kernel-2.0.0-M06.jar +0 -0
  34. data/lib/neo4j/jars/neo4j-lucene-index-2.0.0-M06.jar +0 -0
  35. data/lib/neo4j/jars/neo4j-management-2.0.0-M06.jar +0 -0
  36. data/lib/neo4j/jars/org.apache.servicemix.bundles.jline-0.9.94_1.jar +0 -0
  37. data/lib/neo4j/jars/parboiled-core-1.1.6.jar +0 -0
  38. data/lib/neo4j/jars/parboiled-scala_2.10-1.1.6.jar +0 -0
  39. data/lib/neo4j/jars/scala-library-2.10.2.jar +0 -0
  40. data/lib/neo4j/label.rb +88 -0
  41. data/lib/neo4j/node.rb +185 -0
  42. data/lib/neo4j/property_container.rb +22 -0
  43. data/lib/neo4j/property_validator.rb +23 -0
  44. data/lib/neo4j/relationship.rb +84 -0
  45. data/lib/neo4j/session.rb +124 -0
  46. data/lib/neo4j/tasks/neo4j_server.rb +131 -0
  47. data/lib/neo4j/transaction.rb +52 -0
  48. data/neo4j-core.gemspec +35 -0
  49. metadata +144 -0
@@ -0,0 +1,156 @@
1
+ module Neo4j::Server
2
+
3
+ # Plugin
4
+ Neo4j::Session.register_db(:server_db) do |endpoint_url|
5
+ response = HTTParty.get(endpoint_url)
6
+ raise "Server not available on #{endpoint_url} (response code #{response.code})" unless response.code == 200
7
+ root_data = JSON.parse(response.body)
8
+ Neo4j::Server::CypherSession.new(root_data['data'])
9
+ end
10
+
11
+ class CypherSession < Neo4j::Session
12
+ include Resource
13
+ include Neo4j::Core::CypherTranslator
14
+
15
+ alias_method :super_query, :query
16
+
17
+ def initialize(data_url)
18
+ Neo4j::Session.register(self)
19
+ initialize_resource(data_url)
20
+ end
21
+
22
+ def to_s
23
+ "CypherSession #{@resource_url}"
24
+ end
25
+
26
+ def initialize_resource(data_url)
27
+ response = HTTParty.get(data_url)
28
+ expect_response_code(response,200)
29
+ data_resource = JSON.parse(response.body)
30
+ raise "!!!!NO data_resource for #{response.body}" unless data_resource
31
+ # store the resource data
32
+ init_resource_data(data_resource, data_url)
33
+ end
34
+
35
+ def close
36
+ super
37
+ Neo4j::Transaction.unregister_current
38
+ end
39
+
40
+ def begin_tx
41
+ tx = wrap_resource(self, 'transaction', CypherTransaction, nil, :post)
42
+ Thread.current[:neo4j_curr_tx] = tx
43
+ tx
44
+ end
45
+
46
+ def create_node(props=nil, labels=[])
47
+ l = labels.empty? ? "" : ":" + labels.map{|k| "`#{k}`"}.join(':')
48
+ q = "CREATE (n#{l} #{cypher_prop_list(props)}) RETURN ID(n)"
49
+ cypher_response = _query_or_fail(q, true)
50
+ CypherNode.new(self, cypher_response)
51
+ end
52
+
53
+ def load_node(neo_id)
54
+ cypher_response = _query("START n=node(#{neo_id}) RETURN n")
55
+ if (!cypher_response.error?)
56
+ CypherNode.new(self, neo_id)
57
+ elsif (cypher_response.error_status == 'EntityNotFoundException')
58
+ return nil
59
+ else
60
+ cypher_response.raise_error
61
+ end
62
+ end
63
+
64
+ def load_relationship(neo_id)
65
+ cypher_response = _query("START r=relationship(#{neo_id}) RETURN r")
66
+ if (!cypher_response.error?)
67
+ CypherRelationship.new(self, neo_id)
68
+ elsif (cypher_response.error_msg =~ /not found/) # Ugly that the Neo4j API gives us this error message
69
+ return nil
70
+ else
71
+ cypher_response.raise_error
72
+ end
73
+ end
74
+
75
+ def create_label(name)
76
+ CypherLabel.new(self, name)
77
+ end
78
+
79
+ def indexes(label)
80
+ response = HTTParty.get("#{@resource_url}schema/index/#{label}")
81
+ expect_response_code(response, 200)
82
+ data_resource = JSON.parse(response.body)
83
+
84
+ property_keys = data_resource.map do |row|
85
+ row['property-keys'].map(&:to_sym)
86
+ end
87
+
88
+ {
89
+ property_keys: property_keys
90
+ }
91
+ end
92
+
93
+ def find_all_nodes(label_name)
94
+ response = _query_or_fail("MATCH (n:`#{label_name}`) RETURN ID(n)")
95
+ search_result_to_enumerable(response)
96
+ end
97
+
98
+ def find_nodes(label_name, key, value)
99
+ response = _query_or_fail <<-CYPHER
100
+ MATCH (n:`#{label_name}`)
101
+ WHERE n.#{key} = '#{value}'
102
+ RETURN ID(n)
103
+ CYPHER
104
+ search_result_to_enumerable(response)
105
+ end
106
+
107
+ def query(*params, &query_dsl)
108
+ result = super
109
+ if result.error?
110
+ raise Neo4j::Session::CypherError.new(result.error_msg, result.error_code, result.error_status)
111
+ end
112
+ result.to_hash_enumeration
113
+ end
114
+
115
+ # TODO remove this function and do not use cypher DSL internally
116
+ def _query_internal(*params, &query_dsl)
117
+ super_query(*params, &query_dsl)
118
+ end
119
+
120
+ def _query_or_fail(q, single_row = false, params=nil)
121
+ response = _query(q, params)
122
+ response.raise_error if response.error?
123
+ single_row ? response.first_data : response
124
+ end
125
+
126
+ def query_default_return
127
+ " RETURN ID(n)"
128
+ end
129
+
130
+ def _query(q, params=nil)
131
+ curr_tx = Neo4j::Transaction.current
132
+ if (curr_tx)
133
+ curr_tx._query(q, params)
134
+ else
135
+ url = resource_url('cypher')
136
+ q = params.nil? ? {query: q} : {query: q, params: params}
137
+ response = HTTParty.post(url, headers: resource_headers, body: q.to_json)
138
+ CypherResponse.create_with_no_tx(response)
139
+ end
140
+ end
141
+
142
+ def search_result_to_enumerable(response)
143
+ return [] unless response.data
144
+
145
+ Enumerator.new do |yielder|
146
+ response.data.each do |data|
147
+ yielder << CypherNode.new(self, data[0]).wrapper
148
+ end
149
+ end
150
+ end
151
+
152
+
153
+
154
+
155
+ end
156
+ end
@@ -0,0 +1,81 @@
1
+ module Neo4j::Server
2
+ class CypherTransaction
3
+ attr_reader :commit_url, :exec_url
4
+
5
+ include Resource
6
+ include Neo4j::Core::CypherTranslator
7
+
8
+ class CypherError < StandardError
9
+ attr_reader :code, :status
10
+ def initialize(code, status, message)
11
+ super(message)
12
+ @code = code
13
+ @status = status
14
+ end
15
+ end
16
+
17
+ def initialize(db, response, url)
18
+ @commit_url = response['commit']
19
+ @exec_url = response.headers['location']
20
+ init_resource_data(response, url)
21
+ expect_response_code(response,201)
22
+ Neo4j::Transaction.register(self)
23
+ end
24
+
25
+ def _query(cypher_query, params=nil)
26
+ statement = {statement: cypher_query}
27
+ body = {statements: [statement]}
28
+
29
+ if params
30
+ # TODO can't get this working for some reason using parameters
31
+ #props = params.keys.inject({}) do|ack, k|
32
+ # ack[k] = {name: params[k]}
33
+ # ack
34
+ #end
35
+ #statement[:parameters] = props
36
+
37
+ # So we have to do this workaround
38
+ params.each_pair do |k,v|
39
+ statement[:statement].gsub!("{ #{k} }", escape_value(v))
40
+ end
41
+ end
42
+ response = HTTParty.post(@exec_url, headers: resource_headers, body: body.to_json)
43
+
44
+ first_result = response['results'][0]
45
+ cr = CypherResponse.new(response, true)
46
+
47
+ if (response['errors'].empty?)
48
+ cr.set_data(first_result['data'], first_result['columns'])
49
+ else
50
+ first_error = response['errors'].first
51
+ cr.set_error(first_error['message'], first_error['status'], first_error['code'])
52
+ end
53
+ cr
54
+ end
55
+
56
+ def success
57
+ # this is need in the Java API
58
+ end
59
+
60
+ def failure
61
+ @failure = true
62
+ end
63
+
64
+ def failure?
65
+ !!@failure
66
+ end
67
+
68
+ def finish
69
+ Neo4j::Transaction.unregister(self)
70
+ if failure?
71
+ response = HTTParty.delete(@exec_url, headers: resource_headers)
72
+ else
73
+ response = HTTParty.post(@commit_url, headers: resource_headers)
74
+ end
75
+ expect_response_code(response,200)
76
+ response
77
+ end
78
+
79
+
80
+ end
81
+ end
@@ -0,0 +1,73 @@
1
+ module Neo4j
2
+ module Server
3
+ module Resource
4
+
5
+ class ServerException < Exception
6
+ end
7
+
8
+ attr_reader :resource_data, :resource_url
9
+
10
+ def init_resource_data(resource_data, resource_url)
11
+ raise "Exception #{response['exception']}" if resource_data['exception']
12
+ @resource_url = resource_url
13
+ @resource_data = resource_data
14
+ raise "expected @resource_data to be Hash got #{@resource_data.class}" unless @resource_data.respond_to?(:[])
15
+ self
16
+ end
17
+
18
+
19
+ def wrap_resource(db, rel, resource_class, args=nil, verb=:get, payload=nil)
20
+ url = resource_url(rel, args)
21
+ response = HTTParty.send(verb, url, headers: {'Content-Type' => 'application/json'}, body: payload)
22
+ response.code == 404 ? nil : resource_class.new(db, response, url)
23
+ end
24
+
25
+ def resource_url(rel=nil, args=nil)
26
+ return @resource_url unless rel
27
+ url = @resource_data[rel.to_s]
28
+ raise "No resource rel '#{rel}', available #{@resource_data.keys.inspect}" unless url
29
+ return url unless args
30
+ if (args.is_a?(Hash))
31
+ args.keys.inject(url){|ack, key| ack.sub("{#{key}}",args[key].to_s)}
32
+ else
33
+ "#{url}/#{args.to_s}"
34
+ end
35
+ end
36
+
37
+ def handle_response_error(response, msg="Error for request", url = response.request.path.to_s )
38
+ raise ServerException.new("#{msg} #{url}, #{response.code}, #{response.body}")
39
+ end
40
+
41
+ def expect_response_code(response, expected_code, msg="Error for request", url=response.request.path.to_s )
42
+ handle_response_error(response, "Expected response code #{expected_code} #{msg}",url) unless response.code == expected_code
43
+ response
44
+ end
45
+
46
+ def response_exception(response)
47
+ return nil if response.body.nil? || response.body.empty?
48
+ JSON.parse(response.body)['exception']
49
+ end
50
+
51
+ def resource_headers
52
+ {'Content-Type' => 'application/json', 'Accept' => 'application/json'}
53
+ end
54
+
55
+ def resource_url_id(url = @resource_url)
56
+ url.match(/\/(\d+)$/)[1].to_i
57
+ end
58
+
59
+ def convert_from_json_value(value)
60
+ JSON.parse(value, :quirks_mode => true)
61
+ end
62
+
63
+ def convert_to_json_value(value)
64
+ case value
65
+ when String
66
+ %Q["#{value}"]
67
+ else
68
+ value.to_s
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ module Neo4j
2
+ module EntityEquality
3
+ def ==(o)
4
+ o.class == self.class && o.neo_id == neo_id
5
+ end
6
+ alias_method :eql?, :==
7
+
8
+ end
9
+ end
@@ -0,0 +1,88 @@
1
+ module Neo4j
2
+ class Label
3
+
4
+ # @abstract
5
+ def name
6
+ raise 'not implemented'
7
+ end
8
+
9
+ # @abstract
10
+ def create_index(*properties)
11
+ raise 'not implemented'
12
+ end
13
+
14
+ # @abstract
15
+ def drop_index(*properties)
16
+ raise 'not implemented'
17
+ end
18
+
19
+ # List indices for a label
20
+ # @abstract
21
+ def indexes
22
+ raise 'not implemented'
23
+ end
24
+
25
+ class << self
26
+ include Neo4j::Core::CypherTranslator
27
+
28
+ def create(name, session = Neo4j::Session.current)
29
+ session.create_label(name)
30
+ end
31
+
32
+ def query(label_name, query, session = Neo4j::Session.current)
33
+ cypher = "MATCH (n:`#{label_name}`)"
34
+ cypher += condition_to_cypher(query) if query[:conditions] && !query[:conditions].empty?
35
+ cypher += session.query_default_return
36
+ cypher += order_to_cypher(query) if query[:order]
37
+
38
+ response = session._query_or_fail(cypher)
39
+ session.search_result_to_enumerable(response) # TODO make it work in Embedded and refactor
40
+ end
41
+
42
+
43
+ def find_all_nodes(label_name, session = Neo4j::Session.current)
44
+ session.find_all_nodes(label_name)
45
+ end
46
+
47
+ def find_nodes(label_name, key, value, session = Neo4j::Session.current)
48
+ session.find_nodes(label_name, key, value)
49
+ end
50
+
51
+ private
52
+
53
+ def condition_to_cypher(query)
54
+ conditions = query[:conditions]
55
+ " WHERE " + conditions.keys.map do |k|
56
+ "n.#{k}=#{escape_value(conditions[k])}"
57
+ end.join(" AND ")
58
+ end
59
+
60
+ def order_to_cypher(query)
61
+ cypher = " ORDER BY "
62
+ order = query[:order]
63
+
64
+ handleHash = Proc.new do |hash|
65
+ if (hash.is_a?(Hash))
66
+ k, v = hash.first
67
+ raise "only :asc or :desc allowed in order, got #{query.inspect}" unless [:asc, :desc].include?(v)
68
+ v.to_sym == :asc ? "n.`#{k}`" : "n.`#{k}` DESC"
69
+ else
70
+ "n.`#{hash}`" unless hash.is_a?(Hash)
71
+ end
72
+ end
73
+
74
+ case order
75
+ when Array
76
+ cypher += order.map(&handleHash).join(', ')
77
+ when Hash
78
+ cypher += handleHash.call(order)
79
+ else
80
+ cypher += "n.`#{order}`"
81
+ end
82
+
83
+ cypher
84
+ end
85
+ end
86
+ end
87
+
88
+ end
data/lib/neo4j/node.rb ADDED
@@ -0,0 +1,185 @@
1
+ module Neo4j
2
+
3
+ # A module that allows plugins to register wrappers around Neo4j::Node objects
4
+ module Wrapper
5
+ # Used by Neo4j::NodeMixin to wrap nodes
6
+ def wrapper
7
+ self
8
+ end
9
+ end
10
+
11
+ # The base class for both the Embedded and Server Neo4j Node
12
+ # Notice this class is abstract and can't be instantiated
13
+ class Node
14
+ include EntityEquality
15
+ include Wrapper
16
+ include PropertyContainer
17
+
18
+ # @return [Hash] all properties of the node
19
+ def props()
20
+ raise 'not implemented'
21
+ end
22
+
23
+ # replace all properties with new properties
24
+ # @param hash a hash of properties the node should have
25
+ def props=(hash)
26
+ raise 'not implemented'
27
+ end
28
+
29
+ # Directly remove the property on the node (low level method, may need transaction)
30
+ def remove_property(key)
31
+ raise 'not implemented'
32
+ end
33
+
34
+ # Directly set the property on the node (low level method, may need transaction)
35
+ # @param [Hash, String] key
36
+ # @param value see Neo4j::PropertyValidator::VALID_PROPERTY_VALUE_CLASSES for valid values
37
+ def set_property(key, value)
38
+ raise 'not implemented'
39
+ end
40
+
41
+ # Directly get the property on the node (low level method, may need transaction)
42
+ # @param [Hash, String] key
43
+ # @return the value of the key
44
+ def get_property(key, value)
45
+ raise 'not implemented'
46
+ end
47
+
48
+ # Creates a relationship of given type to other_node with optionally properties
49
+ # @param [Symbol] type the type of the relation between the two nodes
50
+ # @param [Neo4j::Node] other_node the other node
51
+ # @param [Hash] props optionally properties for the created relationship
52
+ def create_rel(type, other_node, props = nil)
53
+ raise 'not implemented'
54
+ end
55
+
56
+
57
+ # Returns an enumeration of relationships.
58
+ # It always returns relationships of depth one.
59
+ #
60
+ # @param [Hash] opts the options to create a message with.
61
+ # @option opts [Symbol] :dir dir the direction of the relationship, allowed values: :both, :incoming, :outgoing.
62
+ # @option opts [Symbol] :type the type of relationship to navigate
63
+ # @option opts [Symbol] :between return all the relationships between this and given node
64
+ # @return [Enumerable] of Neo4j::Relationship objects
65
+ #
66
+ # @example Return both incoming and outgoing relationships of any type
67
+ # node_a.rels
68
+ #
69
+ # @example All outgoing or incoming relationship of type friends
70
+ # node_a.rels(type: :friends)
71
+ #
72
+ # @example All outgoing relationships between me and another node of type friends
73
+ # node_a.rels(type: :friends, dir: :outgoing, between: node_b)
74
+ #
75
+ def rels(match = {dir: :both})
76
+ raise 'not implemented'
77
+ end
78
+
79
+ # Adds one or more Neo4j labels on the node
80
+ def add_label(*labels)
81
+ raise 'not implemented'
82
+ end
83
+
84
+ # @return all labels on the node
85
+ def labels()
86
+ raise 'not implemented'
87
+ end
88
+
89
+ # Deletes this node from the database
90
+ def del()
91
+ raise 'not implemented'
92
+ end
93
+
94
+ # @return true if the node exists in the database
95
+ def exist?
96
+ raise 'not implemented'
97
+ end
98
+
99
+ # @returns all the Neo4j labels for this node
100
+ def labels
101
+ raise 'not implemented'
102
+ end
103
+
104
+ # Returns the only node of a given type and direction that is attached to this node, or nil.
105
+ # This is a convenience method that is used in the commonly occuring situation where a node has exactly zero or one relationships of a given type and direction to another node.
106
+ # Typically this invariant is maintained by the rest of the code: if at any time more than one such relationships exist, it is a fatal error that should generate an exception.
107
+ #
108
+ # This method reflects that semantics and returns either:
109
+ # * nil if there are zero relationships of the given type and direction,
110
+ # * the relationship if there's exactly one, or
111
+ # * throws an exception in all other cases.
112
+ #
113
+ # This method should be used only in situations with an invariant as described above. In those situations, a "state-checking" method (e.g. #rel?) is not required,
114
+ # because this method behaves correctly "out of the box."
115
+ #
116
+ # @param (see #rel)
117
+ def node(specs = {})
118
+ raise 'not implemented'
119
+ end
120
+
121
+ # Same as #node but returns the relationship. Notice it may raise an exception if there are more then one relationship matching.
122
+ def rel(spec = {})
123
+ raise 'not implemented'
124
+ end
125
+
126
+ # Returns true or false if there is one or more relationships
127
+ # Same as `!! #rel()`
128
+ def rel?(spec = {})
129
+ raise 'not implemented'
130
+ end
131
+
132
+ # Same as Neo4j::Node#exist?
133
+ def exist?
134
+ raise 'not implemented'
135
+ end
136
+
137
+ # Works like #rels method but instead returns the nodes.
138
+ # It does try to load a Ruby wrapper around each node
139
+ # @abstract
140
+ # @param (see #rels)
141
+ # @return [Enumerable] an Enumeration of either Neo4j::Node objects or wrapped Neo4j::Node objects
142
+ # @notice it's possible that the same node is returned more then once because of several relationship reaching to the same node, see #outgoing for alternative
143
+ def nodes(specs = {})
144
+ #rels(specs).map{|n| n.other_node(self)}
145
+ end
146
+
147
+ class << self
148
+ # Creates a node
149
+ def create(props=nil, *labels_or_db)
150
+ session = Neo4j::Core::ArgumentHelper.session(labels_or_db)
151
+ session.create_node(props, labels_or_db)
152
+ end
153
+
154
+ # Loads a node from the database with given id
155
+ def load(neo_id, session = Neo4j::Session.current)
156
+ node = session.load_node(neo_id)
157
+ node && node.wrapper
158
+ end
159
+
160
+ # Checks if the given entity node or entity id (Neo4j::Node#neo_id) exists in the database.
161
+ # @return [true, false] if exist
162
+ def exist?(entity_or_entity_id, session = Neo4j::Session.current)
163
+ session.node_exist?(neo_id)
164
+ end
165
+
166
+ # Find the node with given label and value
167
+ def find_nodes(label, value=nil, session = Neo4j::Session.current)
168
+ session.find_nodes(label, value)
169
+ end
170
+ end
171
+
172
+ def initialize
173
+ raise "Can't instantiate abstract class" if abstract_class?
174
+ puts "Instantiated!"
175
+ end
176
+
177
+ private
178
+ def abstract_class?
179
+ self.class == Node
180
+ end
181
+
182
+
183
+ end
184
+
185
+ end