neo4j 3.0.0.alpha.8 → 3.0.0.alpha.9
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.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/Gemfile +1 -1
- data/config/neo4j/config.yml +19 -84
- data/lib/neo4j.rb +5 -2
- data/lib/neo4j.rb~ +40 -0
- data/lib/neo4j/active_node.rb +19 -1
- data/lib/neo4j/active_node/has_n.rb +64 -124
- data/lib/neo4j/active_node/has_n/association.rb +142 -0
- data/lib/neo4j/active_node/id_property.rb +119 -0
- data/lib/neo4j/active_node/identity.rb +0 -5
- data/lib/neo4j/active_node/initialize.rb +1 -0
- data/lib/neo4j/active_node/labels.rb +62 -25
- data/lib/neo4j/active_node/persistence.rb +26 -5
- data/lib/neo4j/active_node/property.rb +121 -18
- data/lib/neo4j/active_node/query.rb +17 -9
- data/lib/neo4j/active_node/query/query_proxy.rb +202 -46
- data/lib/neo4j/active_node/serialized_properties.rb +21 -0
- data/lib/neo4j/config.rb +119 -0
- data/lib/neo4j/paginated.rb +19 -0
- data/lib/neo4j/railtie.rb +1 -0
- data/lib/neo4j/type_converters.rb +49 -3
- data/lib/neo4j/version.rb +1 -1
- data/lib/neo4j/wrapper.rb +15 -10
- data/lib/person.rb~ +9 -0
- data/lib/rails/generators/neo4j_generator.rb +1 -1
- data/lib/test.rb +21 -0
- data/lib/test.rb~ +21 -0
- data/neo4j.gemspec +2 -1
- metadata +27 -5
- data/lib/neo4j/active_node/has_n/decl_rel.rb +0 -252
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'active_support/inflector/inflections'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module ActiveNode
|
5
|
+
module HasN
|
6
|
+
class Association
|
7
|
+
attr_reader :type, :name, :relationship, :direction
|
8
|
+
|
9
|
+
def initialize(type, direction, name, options = {})
|
10
|
+
raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym)
|
11
|
+
raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym)
|
12
|
+
@type = type.to_sym
|
13
|
+
@name = name
|
14
|
+
@direction = direction.to_sym
|
15
|
+
raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin]
|
16
|
+
|
17
|
+
@target_class_name_from_name = name.to_s.classify
|
18
|
+
@target_class_option = target_class_option(options)
|
19
|
+
@callbacks = {before: options[:before], after: options[:after]}
|
20
|
+
@relationship_type = options[:type] && options[:type].to_sym
|
21
|
+
@origin = options[:origin] && options[:origin].to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
def target_class_option(options)
|
25
|
+
if options[:model_class].nil?
|
26
|
+
@target_class_name_from_name
|
27
|
+
elsif options[:model_class]
|
28
|
+
options[:model_class]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
|
33
|
+
def arrow_cypher(var = nil, properties = {}, create = false)
|
34
|
+
validate_origin!
|
35
|
+
|
36
|
+
relationship_type = relationship_type(create)
|
37
|
+
relationship_name_cypher = ":`#{relationship_type}`" if relationship_type
|
38
|
+
|
39
|
+
properties_string = get_properties_string(properties)
|
40
|
+
relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string)
|
41
|
+
get_direction(relationship_cypher, create)
|
42
|
+
end
|
43
|
+
|
44
|
+
def target_class_name
|
45
|
+
@target_class_option.to_s if @target_class_option
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_class
|
49
|
+
return @target_class if @target_class
|
50
|
+
|
51
|
+
@target_class = target_class_name.constantize if target_class_name
|
52
|
+
rescue NameError
|
53
|
+
raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified"
|
54
|
+
end
|
55
|
+
|
56
|
+
def callback(type)
|
57
|
+
@callbacks[type]
|
58
|
+
end
|
59
|
+
|
60
|
+
def perform_callback(caller, other_node, type)
|
61
|
+
return if callback(type).nil?
|
62
|
+
caller.send(callback(type), other_node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def relationship_type(create = false)
|
66
|
+
if @relationship_type
|
67
|
+
@relationship_type
|
68
|
+
elsif @origin
|
69
|
+
origin_type
|
70
|
+
else
|
71
|
+
(create || exceptional_target_class?) && "##{@name}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def get_direction(relationship_cypher, create)
|
78
|
+
dir = (create && @direction == :both) ? :out : @direction
|
79
|
+
case dir
|
80
|
+
when :out
|
81
|
+
"-#{relationship_cypher}->"
|
82
|
+
when :in
|
83
|
+
"<-#{relationship_cypher}-"
|
84
|
+
when :both
|
85
|
+
"-#{relationship_cypher}-"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_relationship_cypher(var, relationship_name_cypher, properties_string)
|
90
|
+
"[#{var}#{relationship_name_cypher}#{properties_string}]"
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_properties_string(properties)
|
94
|
+
p = properties.map do |key, value|
|
95
|
+
"#{key}: #{value.inspect}"
|
96
|
+
end.join(', ')
|
97
|
+
p.size == 0 ? '' : " {#{p}}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def origin_type
|
101
|
+
target_class.associations[@origin].relationship_type
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return basic details about association as declared in the model
|
105
|
+
# @example
|
106
|
+
# has_many :in, :bands
|
107
|
+
def base_declaration
|
108
|
+
"#{type} #{direction.inspect}, #{name.inspect}"
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Determine if model class as derived from the association name would be different than the one specified via the model_class key
|
113
|
+
# @example
|
114
|
+
# has_many :friends # Would return false
|
115
|
+
# has_many :friends, model_class: Friend # Would return false
|
116
|
+
# has_many :friends, model_class: Person # Would return true
|
117
|
+
def exceptional_target_class?
|
118
|
+
# TODO: Exceptional if target_class.nil?? (when model_class false)
|
119
|
+
|
120
|
+
target_class && target_class.name != @target_class_name_from_name
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_origin!
|
124
|
+
if @origin
|
125
|
+
if target_class
|
126
|
+
if association = target_class.associations[@origin]
|
127
|
+
if @direction == association.direction
|
128
|
+
raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
|
129
|
+
end
|
130
|
+
else
|
131
|
+
raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
|
132
|
+
end
|
133
|
+
else
|
134
|
+
raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Neo4j::ActiveNode
|
2
|
+
|
3
|
+
# This module makes it possible to use other IDs than the build it neo4j id (neo_id)
|
4
|
+
#
|
5
|
+
# @example using generated UUIDs
|
6
|
+
# class Person
|
7
|
+
# include Neo4j::ActiveNode
|
8
|
+
# # creates a 'secret' neo4j property my_uuid which will be used as primary key
|
9
|
+
# id_property :my_uuid, auto: :uuid
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# @example using user defined ids
|
13
|
+
# class Person
|
14
|
+
# include Neo4j::ActiveNode
|
15
|
+
# property :title
|
16
|
+
# validates :title, :presence => true
|
17
|
+
# id_property :title_id, on: :title_to_url
|
18
|
+
#
|
19
|
+
# def title_to_url
|
20
|
+
# self.title.urlize # uses https://github.com/cheef/string-urlize gem
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
module IdProperty
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
|
28
|
+
module TypeMethods
|
29
|
+
def define_id_methods(clazz, name, conf)
|
30
|
+
validate_conf(conf)
|
31
|
+
if conf[:on]
|
32
|
+
define_custom_method(clazz, name, conf[:on])
|
33
|
+
elsif conf[:auto]
|
34
|
+
raise "only :uuid auto id_property allowed, got #{conf[:auto]}" unless conf[:auto] == :uuid
|
35
|
+
define_uuid_method(clazz, name)
|
36
|
+
else conf.empty?
|
37
|
+
define_property_method(clazz, name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def validate_conf(conf)
|
44
|
+
return if conf.empty?
|
45
|
+
|
46
|
+
raise "Expected a Hash, got #{conf.class} (#{conf.to_s}) for id_property" unless conf.is_a?(Hash)
|
47
|
+
|
48
|
+
unless conf.include?(:auto) || conf.include?(:on)
|
49
|
+
raise "Illegal value #{conf.inspect} for id_property, expected :on or :auto"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_property_method(clazz, name)
|
54
|
+
clazz.module_eval(%Q{
|
55
|
+
def id
|
56
|
+
persisted? ? #{name} : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
property :#{name}
|
60
|
+
validates_uniqueness_of :#{name}
|
61
|
+
}, __FILE__, __LINE__)
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def define_uuid_method(clazz, name)
|
66
|
+
clazz.module_eval(%Q{
|
67
|
+
default_property :#{name} do
|
68
|
+
::SecureRandom.uuid
|
69
|
+
end
|
70
|
+
|
71
|
+
def #{name}
|
72
|
+
default_property :#{name}
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :id, :#{name}
|
76
|
+
}, __FILE__, __LINE__)
|
77
|
+
end
|
78
|
+
|
79
|
+
def define_custom_method(clazz, name, on)
|
80
|
+
clazz.module_eval(%Q{
|
81
|
+
default_property :#{name} do |instance|
|
82
|
+
raise "Specifying custom id_property #{name} on none existing method #{on}" unless instance.respond_to?(:#{on})
|
83
|
+
instance.#{on}
|
84
|
+
end
|
85
|
+
|
86
|
+
def #{name}
|
87
|
+
default_property :#{name}
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :id, :#{name}
|
91
|
+
}, __FILE__, __LINE__)
|
92
|
+
end
|
93
|
+
|
94
|
+
extend self
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
module ClassMethods
|
99
|
+
|
100
|
+
def find_by_id(key)
|
101
|
+
Neo4j::Node.load(key.to_i)
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def id_property(name, conf = {})
|
106
|
+
TypeMethods.define_id_methods(self, name, conf)
|
107
|
+
|
108
|
+
constraint name, type: :unique
|
109
|
+
|
110
|
+
self.define_singleton_method(:find_by_id) do |key|
|
111
|
+
self.where(name => key).first
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Neo4j::ActiveNode
|
2
2
|
module Identity
|
3
|
-
|
4
3
|
def ==(o)
|
5
4
|
o.class == self.class && o.id == id
|
6
5
|
end
|
@@ -17,13 +16,9 @@ module Neo4j::ActiveNode
|
|
17
16
|
_persisted_node ? _persisted_node.neo_id : nil
|
18
17
|
end
|
19
18
|
|
20
|
-
# @return [String, nil] same as #neo_id
|
21
19
|
def id
|
22
20
|
id = neo_id
|
23
21
|
id.is_a?(Integer) ? id : nil
|
24
22
|
end
|
25
|
-
|
26
|
-
|
27
23
|
end
|
28
|
-
|
29
24
|
end
|
@@ -11,6 +11,7 @@ module Neo4j::ActiveNode::Initialize
|
|
11
11
|
@_persisted_node = persisted_node
|
12
12
|
changed_attributes && changed_attributes.clear
|
13
13
|
@attributes = attributes.merge(properties.stringify_keys)
|
14
|
+
self.default_properties=properties
|
14
15
|
@attributes = convert_properties_to :ruby, @attributes
|
15
16
|
end
|
16
17
|
|
@@ -59,20 +59,19 @@ module Neo4j
|
|
59
59
|
end
|
60
60
|
|
61
61
|
module ClassMethods
|
62
|
-
|
63
62
|
# Find all nodes/objects of this class
|
64
63
|
def all
|
65
64
|
self.query_as(:n).pluck(:n)
|
66
65
|
end
|
67
66
|
|
67
|
+
# Returns the first node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
68
68
|
def first
|
69
|
-
self.query_as(:n).limit(1).order('n
|
69
|
+
self.query_as(:n).limit(1).order('ID(n)').pluck(:n).first
|
70
70
|
end
|
71
71
|
|
72
|
+
# Returns the last node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
72
73
|
def last
|
73
|
-
|
74
|
-
final_count = count == 0 ? 0 : count - 1
|
75
|
-
self.query_as(:n).order('n.neo_id').skip(final_count).limit(1).pluck(:n).first
|
74
|
+
self.query_as(:n).order('ID(n) DESC').limit(1).pluck(:n).first
|
76
75
|
end
|
77
76
|
|
78
77
|
# @return [Fixnum] number of nodes of this class
|
@@ -81,15 +80,14 @@ module Neo4j
|
|
81
80
|
end
|
82
81
|
|
83
82
|
# Returns the object with the specified neo4j id.
|
84
|
-
# @param [String,Fixnum]
|
83
|
+
# @param [String,Fixnum] id of node to find
|
85
84
|
def find(id)
|
86
|
-
raise "Unknown argument #{id.class} in find method" if not [String, Fixnum].include?(id.class)
|
87
|
-
|
88
|
-
Neo4j::Node.load(id.to_i)
|
85
|
+
raise "Unknown argument #{id.class} in find method (expected String or Fixnum)" if not [String, Fixnum].include?(id.class)
|
86
|
+
find_by_id(id)
|
89
87
|
end
|
90
88
|
|
91
89
|
# Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
|
92
|
-
# @param [Hash]
|
90
|
+
# @param [Hash] args of arguments to find
|
93
91
|
def find_by(*args)
|
94
92
|
self.query_as(:n).where(n: eval(args.join)).limit(1).pluck(:n).first
|
95
93
|
end
|
@@ -100,21 +98,51 @@ module Neo4j
|
|
100
98
|
find_by(args) or raise RecordNotFound, "#{self.query_as(:n).where(n: a).limit(1).to_cypher} returned no results"
|
101
99
|
end
|
102
100
|
|
103
|
-
# Destroy all nodes
|
101
|
+
# Destroy all nodes and connected relationships
|
104
102
|
def destroy_all
|
105
103
|
self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`)-[r]-() DELETE n,r")
|
106
104
|
self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`) DELETE n")
|
107
105
|
end
|
108
106
|
|
109
107
|
# Creates a Neo4j index on given property
|
108
|
+
#
|
109
|
+
# This can also be done on the property directly, see Neo4j::ActiveNode::Property::ClassMethods#property.
|
110
|
+
#
|
110
111
|
# @param [Symbol] property the property we want a Neo4j index on
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
112
|
+
# @param [Hash] conf optional property configuration
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# class Person
|
116
|
+
# include Neo4j::ActiveNode
|
117
|
+
# property :name
|
118
|
+
# index :name
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# @example with constraint
|
122
|
+
# class Person
|
123
|
+
# include Neo4j::ActiveNode
|
124
|
+
# property :name
|
125
|
+
#
|
126
|
+
# # below is same as: index :name, index: :exact, constraint: {type: :unique}
|
127
|
+
# index :name, constraint: {type: :unique}
|
128
|
+
# end
|
129
|
+
def index(property, conf = {})
|
130
|
+
Neo4j::Session.on_session_available do |_|
|
131
|
+
_index(property, conf)
|
132
|
+
end
|
133
|
+
@_indexed_properties ||= []
|
134
|
+
@_indexed_properties.push property unless @_indexed_properties.include? property
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates a neo4j constraint on this class for given property
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# Person.constraint :name, type: :unique
|
141
|
+
#
|
142
|
+
def constraint(property, constraints, session = Neo4j::Session.current)
|
143
|
+
Neo4j::Session.on_session_available do |_|
|
144
|
+
label = Neo4j::Label.create(mapped_label_name)
|
145
|
+
label.create_constraint(property, constraints, session)
|
118
146
|
end
|
119
147
|
end
|
120
148
|
|
@@ -132,17 +160,30 @@ module Neo4j
|
|
132
160
|
@_label_name || self.to_s.to_sym
|
133
161
|
end
|
134
162
|
|
135
|
-
|
163
|
+
# @return [Neo4j::Label] the label for this class
|
164
|
+
def mapped_label
|
165
|
+
Neo4j::Label.create(mapped_label_name)
|
166
|
+
end
|
136
167
|
|
168
|
+
def indexed_properties
|
169
|
+
@_indexed_properties
|
137
170
|
end
|
138
171
|
|
172
|
+
|
139
173
|
protected
|
140
174
|
|
141
|
-
def _index(property)
|
175
|
+
def _index(property, conf)
|
142
176
|
mapped_labels.each do |label|
|
143
177
|
# make sure the property is not indexed twice
|
144
178
|
existing = label.indexes[:property_keys]
|
145
|
-
|
179
|
+
|
180
|
+
# In neo4j constraint automatically creates an index
|
181
|
+
if conf[:constraint]
|
182
|
+
constraint(property, conf[:constraint])
|
183
|
+
else
|
184
|
+
label.create_index(property) unless existing.flatten.include?(property)
|
185
|
+
end
|
186
|
+
|
146
187
|
end
|
147
188
|
end
|
148
189
|
|
@@ -150,10 +191,6 @@ module Neo4j
|
|
150
191
|
mapped_label_names.map{|label_name| Neo4j::Label.create(label_name)}
|
151
192
|
end
|
152
193
|
|
153
|
-
def mapped_label
|
154
|
-
Neo4j::Label.create(mapped_label_name)
|
155
|
-
end
|
156
|
-
|
157
194
|
def set_mapped_label_name(name)
|
158
195
|
@_label_name = name.to_sym
|
159
196
|
end
|
@@ -28,6 +28,7 @@ module Neo4j::ActiveNode
|
|
28
28
|
# @return true
|
29
29
|
def create_model(*)
|
30
30
|
set_timestamps
|
31
|
+
create_magic_properties
|
31
32
|
properties = convert_properties_to :db, props
|
32
33
|
node = _create_node(properties)
|
33
34
|
init_on_load(node, node.props)
|
@@ -129,7 +130,9 @@ module Neo4j::ActiveNode
|
|
129
130
|
|
130
131
|
def _create_node(*args)
|
131
132
|
session = self.class.neo4j_session
|
132
|
-
props =
|
133
|
+
props = self.class.default_property_values(self)
|
134
|
+
props.merge!(args[0]) if args[0].is_a?(Hash)
|
135
|
+
set_classname(props)
|
133
136
|
labels = self.class.mapped_label_names
|
134
137
|
session.create_node(props, labels)
|
135
138
|
end
|
@@ -193,15 +196,25 @@ module Neo4j::ActiveNode
|
|
193
196
|
end
|
194
197
|
alias_method :update_attributes!, :update!
|
195
198
|
|
199
|
+
def cache_key
|
200
|
+
if self.new_record?
|
201
|
+
"#{self.class.model_name.cache_key}/new"
|
202
|
+
elsif self.respond_to?(:updated_at) && !self.updated_at.blank?
|
203
|
+
"#{self.class.model_name.cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
|
204
|
+
else
|
205
|
+
"#{self.class.model_name.cache_key}/#{neo_id}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
196
209
|
module ClassMethods
|
197
210
|
# Creates a saves a new node
|
198
211
|
# @param [Hash] props the properties the new node should have
|
199
212
|
def create(props = {})
|
200
|
-
|
213
|
+
association_props = extract_association_attributes!(props)
|
201
214
|
|
202
215
|
new(props).tap do |obj|
|
203
216
|
obj.save
|
204
|
-
|
217
|
+
association_props.each do |prop, value|
|
205
218
|
obj.send("#{prop}=", value)
|
206
219
|
end
|
207
220
|
end
|
@@ -210,12 +223,12 @@ module Neo4j::ActiveNode
|
|
210
223
|
# Same as #create, but raises an error if there is a problem during save.
|
211
224
|
def create!(*args)
|
212
225
|
props = args[0] || {}
|
213
|
-
|
226
|
+
association_props = extract_association_attributes!(props)
|
214
227
|
|
215
228
|
new(*args).tap do |o|
|
216
229
|
yield o if block_given?
|
217
230
|
o.save!
|
218
|
-
|
231
|
+
association_props.each do |prop, value|
|
219
232
|
o.send("#{prop}=", value)
|
220
233
|
end
|
221
234
|
end
|
@@ -229,10 +242,18 @@ module Neo4j::ActiveNode
|
|
229
242
|
|
230
243
|
private
|
231
244
|
|
245
|
+
def create_magic_properties
|
246
|
+
|
247
|
+
end
|
248
|
+
|
232
249
|
def update_magic_properties
|
233
250
|
self.updated_at = DateTime.now if respond_to?(:updated_at=) && changed?
|
234
251
|
end
|
235
252
|
|
253
|
+
def set_classname(props)
|
254
|
+
props[Neo4j::Config.class_name_property] = self.class.name if self.class.cached_class?
|
255
|
+
end
|
256
|
+
|
236
257
|
def assign_attributes(attributes)
|
237
258
|
attributes.each do |attribute, value|
|
238
259
|
send("#{attribute}=", value)
|