neo4j-core 3.0.0.alpha.1

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 (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