active-orient 0.79 → 0.80
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.graphs.txt.swp +0 -0
- data/Gemfile +2 -6
- data/README.md +29 -27
- data/VERSION +1 -1
- data/active-orient.gemspec +4 -3
- data/bin/active-orient-console +18 -6
- data/changelog.md +60 -0
- data/config/connect.yml +8 -8
- data/examples/books.rb +134 -97
- data/graphs.txt +70 -0
- data/lib/active-orient.rb +2 -0
- data/lib/base.rb +38 -17
- data/lib/base_properties.rb +15 -14
- data/lib/class_utils.rb +11 -50
- data/lib/database_utils.rb +23 -22
- data/lib/init.rb +4 -3
- data/lib/model/custom.rb +7 -4
- data/lib/model/e.rb +6 -0
- data/lib/model/edge.rb +74 -30
- data/lib/model/the_class.rb +181 -131
- data/lib/model/the_record.rb +115 -68
- data/lib/model/vertex.rb +261 -126
- data/lib/other.rb +93 -41
- data/lib/rest/change.rb +23 -20
- data/lib/rest/create.rb +71 -63
- data/lib/rest/delete.rb +80 -64
- data/lib/rest/operations.rb +79 -68
- data/lib/rest/read.rb +42 -24
- data/lib/rest/rest.rb +38 -30
- data/lib/support/conversions.rb +42 -0
- data/lib/support/default_formatter.rb +7 -0
- data/lib/support/errors.rb +41 -0
- data/lib/support/orient.rb +167 -58
- data/lib/support/orientquery.rb +526 -348
- data/lib/support/query.rb +92 -0
- metadata +34 -18
- data/examples/test_commands.rb +0 -97
- data/examples/test_commands_2.rb +0 -59
- data/examples/test_commands_3.rb +0 -55
- data/examples/test_commands_4.rb +0 -33
- data/examples/time_graph.md +0 -162
data/graphs.txt
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Graphen
|
2
|
+
======
|
3
|
+
|
4
|
+
Die Felder `in` und `out` bündeln die Verknüpfungen zu anderen Vertices.
|
5
|
+
|
6
|
+
Die Verknüpfungen erfolgen über `Edges`, die wiederum Eigenschaften haben. Zuerst sind die `Edges` Klassen und
|
7
|
+
unterscheiden sich üder den Namen. Zusätzlich besteht Vererbung.
|
8
|
+
|
9
|
+
Beispielgraph
|
10
|
+
@db.create_vertex_class :v1
|
11
|
+
@db.create_class( :v2 ){ V1 }
|
12
|
+
@db.create_edge_class "e1"
|
13
|
+
@db.create_class( :e2 ){ E1 }
|
14
|
+
@db.create_class( :e3 ){ E1 }
|
15
|
+
|
16
|
+
vertices = (1..10).map{|y| V2.create node: y}
|
17
|
+
E2.create from: V1.create( item: 1 ), to: vertices
|
18
|
+
|
19
|
+
v1 = V.first.to_human => "<V1[#25:0]: out: {E2=>10}, item : 1>"
|
20
|
+
v2 = V.last.to_human => "<V2[#40:0]: in: {E2=>1}, node : 8>"
|
21
|
+
|
22
|
+
Boardmittel von ActiveOrient
|
23
|
+
----------------------------
|
24
|
+
|
25
|
+
## Edges anlisten
|
26
|
+
|
27
|
+
#### Nur Links
|
28
|
+
|
29
|
+
v1.edges => ["#49:0", "#50:0", "#51:0", "#52:0", "#53:0", "#54:0", "#55:0", "#56:0", "#49:1", "#50:1"]
|
30
|
+
v2.edges => ["#56:0"]
|
31
|
+
|
32
|
+
|
33
|
+
#### Auflösung der Objekte
|
34
|
+
|
35
|
+
v2.in.map &:to_human => ["<E2: in : #<V2:0x000000000397fcb8>, out : #<V1:0x0000000002ed0060>>"]
|
36
|
+
|
37
|
+
#### Verfügbare Methoden
|
38
|
+
v2.in v2.in_e v2.in_e2 v2.in_e2= v2.in_edges
|
39
|
+
|
40
|
+
|
41
|
+
### Selektive Auswahl
|
42
|
+
|
43
|
+
Beispiel: TimeGrid
|
44
|
+
|
45
|
+
Der Vertex hat folgende Struktur
|
46
|
+
|
47
|
+
t.to_human
|
48
|
+
=> "<Tag[82:8927]: in: {TG::DAY_OF=>1, TG::GRID_OF=>1}, out: {ML::L_OHLC=>1, TG::GRID_OF=>1}, value : 25>"
|
49
|
+
|
50
|
+
dann listet t.detect_edges :out, /ml/ die Edges der ML-Klassen auf:
|
51
|
+
|
52
|
+
t.detect_edges( :out , /ml/).to_human
|
53
|
+
=> ["<L_OHLC[#151:16] -i-> #161:22 { } -o-> #82:8927>"]
|
54
|
+
|
55
|
+
|
56
|
+
#### Node
|
57
|
+
|
58
|
+
Ein Node ist ein direkt über eine Edge verbundener Vertex
|
59
|
+
|
60
|
+
Q = OrientSupport::OrientQuery
|
61
|
+
s= Q.new projection: "expand( outE('e1').in[node <5])"
|
62
|
+
s.from = '#25:0'
|
63
|
+
s.to_s => select expand( outE('v1').in[node <5]) from #25:0
|
64
|
+
|
65
|
+
Aufruf in OrientQuery: Q.node Edge_class, condition
|
66
|
+
|
67
|
+
Fügt den Node in das Porjections-Array ein.
|
68
|
+
|
69
|
+
|
70
|
+
|
data/lib/active-orient.rb
CHANGED
@@ -31,6 +31,7 @@ require 'active_model'
|
|
31
31
|
require_relative "support/orientquery.rb"
|
32
32
|
require_relative "support/conversions.rb"
|
33
33
|
#require_relative "support/logging.rb"
|
34
|
+
require_relative "support/errors.rb"
|
34
35
|
require_relative "base.rb"
|
35
36
|
require_relative "base_properties.rb"
|
36
37
|
require_relative "support/orient.rb"
|
@@ -55,6 +56,7 @@ require_relative "railtie" if defined?(Rails)
|
|
55
56
|
|
56
57
|
module ActiveOrient
|
57
58
|
mattr_accessor :database
|
59
|
+
mattr_accessor :db_pool
|
58
60
|
mattr_accessor :database_classes
|
59
61
|
mattr_accessor :default_server
|
60
62
|
|
data/lib/base.rb
CHANGED
@@ -29,6 +29,7 @@ module ActiveOrient
|
|
29
29
|
Any Change of the Object is thus synchonized to any allocated variable.
|
30
30
|
=end
|
31
31
|
@@rid_store = Hash.new
|
32
|
+
@@mutex = Mutex.new
|
32
33
|
|
33
34
|
def self.display_rid
|
34
35
|
@@rid_store
|
@@ -43,8 +44,14 @@ thus a string or a Model-Object is accepted
|
|
43
44
|
=end
|
44
45
|
|
45
46
|
def self.remove_rid obj
|
46
|
-
|
47
|
-
|
47
|
+
if obj &.rid.present?
|
48
|
+
@@mutex.synchronize do
|
49
|
+
@@rid_store.delete obj.rid
|
50
|
+
end
|
51
|
+
else
|
52
|
+
logger.error "Cache entry not removed: #{obj} "
|
53
|
+
end
|
54
|
+
end
|
48
55
|
|
49
56
|
def self.get_rid rid
|
50
57
|
rid = rid[1..-1] if rid[0]=='#'
|
@@ -63,17 +70,20 @@ and the cached obj is returned
|
|
63
70
|
|
64
71
|
=end
|
65
72
|
def self.store_rid obj
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
|
74
|
+
@@mutex.synchronize do
|
75
|
+
if obj.rid.present? && obj.rid.rid?
|
76
|
+
if @@rid_store[obj.rid].present?
|
77
|
+
@@rid_store[obj.rid].transfer_content from: obj
|
78
|
+
else
|
79
|
+
@@rid_store[obj.rid] = obj
|
80
|
+
end
|
81
|
+
@@rid_store[obj.rid]
|
82
|
+
else
|
83
|
+
obj
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
77
87
|
|
78
88
|
|
79
89
|
# rails compatibility
|
@@ -91,7 +101,7 @@ The model instance fields are then set automatically from the opts Hash.
|
|
91
101
|
def initialize attributes = {}, opts = {}
|
92
102
|
logger.progname = "ActiveOrient::Base#initialize"
|
93
103
|
@metadata = Hash.new # HashWithIndifferentAccess.new
|
94
|
-
@d = nil
|
104
|
+
@d = nil if RUBY_PLATFORM == 'java' && attributes.is_a?( Document )
|
95
105
|
run_callbacks :initialize do
|
96
106
|
if RUBY_PLATFORM == 'java' && attributes.is_a?( Document )
|
97
107
|
@d = attributes
|
@@ -201,7 +211,17 @@ The model instance fields are then set automatically from the opts Hash.
|
|
201
211
|
|
202
212
|
iv = attributes[key]
|
203
213
|
if my_metadata( key: key) == "t"
|
204
|
-
|
214
|
+
# needed in case of
|
215
|
+
# obj.date = {some-date}
|
216
|
+
# --> perfrom an action on the date without saving prior
|
217
|
+
case iv
|
218
|
+
when String
|
219
|
+
iv =~ /00:00:00/ ? Date.parse(iv) : DateTime.parse(iv)
|
220
|
+
when Date, DateTime
|
221
|
+
iv
|
222
|
+
else
|
223
|
+
raise "incompatable type used: #{iv} (#{iv.class}) -- Date or DateTime required"
|
224
|
+
end
|
205
225
|
elsif my_metadata( key: key) == "x"
|
206
226
|
iv = ActiveOrient::Model.autoload_object iv
|
207
227
|
elsif iv.is_a? Array
|
@@ -227,9 +247,9 @@ The model instance fields are then set automatically from the opts Hash.
|
|
227
247
|
when Array
|
228
248
|
if val.first.is_a?(Hash)
|
229
249
|
v = val.map{ |x| x }
|
230
|
-
OrientSupport::Array.new(work_on: self, work_with: v )
|
250
|
+
OrientSupport::Array.new(work_on: self, work_with: v ){ key_to_sym }
|
231
251
|
else
|
232
|
-
OrientSupport::Array.new(work_on: self, work_with: val )
|
252
|
+
OrientSupport::Array.new(work_on: self, work_with: val ){ key_to_sym }
|
233
253
|
end
|
234
254
|
when Hash
|
235
255
|
if val.keys.include?("@class" )
|
@@ -280,6 +300,7 @@ The model instance fields are then set automatically from the opts Hash.
|
|
280
300
|
# end
|
281
301
|
#
|
282
302
|
=begin
|
303
|
+
(Experimental)
|
283
304
|
Exclude some properties from loading via get, reload!, get_document, get_record
|
284
305
|
=end
|
285
306
|
def self.exclude_the_following_properties *args
|
data/lib/base_properties.rb
CHANGED
@@ -8,19 +8,19 @@ module ActiveOrient
|
|
8
8
|
|
9
9
|
# Default presentation of ActiveOrient::Model-Objects
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
def to_human
|
12
|
+
"<#{self.class.to_s.demodulize}: " + content_attributes.map do |attr, value|
|
13
|
+
v= case value
|
14
|
+
when ActiveOrient::Model
|
15
|
+
"< #{self.class.to_.demodulize} : #{value.rrid} >"
|
16
|
+
when OrientSupport::Array
|
17
|
+
value.rrid #.to_human #.map(&:to_human).join("::")
|
18
|
+
else
|
19
|
+
value.from_orient
|
20
|
+
end
|
21
|
+
"%s : %s" % [ attr, v] unless v.nil?
|
22
|
+
end.compact.sort.join(', ') + ">".gsub('"' , ' ')
|
23
|
+
end
|
24
24
|
|
25
25
|
# Comparison support
|
26
26
|
|
@@ -30,8 +30,9 @@ module ActiveOrient
|
|
30
30
|
attr.to_s =~ /(_count)\z/ || attr.to_s =~ /^in_/ || attr.to_s =~ /^out_/ || [:created_at, :updated_at, :type, :id, :order_id, :contract_id].include?(attr.to_sym)
|
31
31
|
end]
|
32
32
|
end
|
33
|
-
|
34
33
|
# return a string ready to include as embedded document
|
34
|
+
# used by Model.to_or
|
35
|
+
#
|
35
36
|
def embedded
|
36
37
|
{ "@type" => 'd', "@class" => self.class.ref_name }
|
37
38
|
.merge(content_attributes)
|
data/lib/class_utils.rb
CHANGED
@@ -3,6 +3,8 @@ module ClassUtils
|
|
3
3
|
|
4
4
|
=begin
|
5
5
|
Returns a valid database-class name, nil if the class does not exists
|
6
|
+
|
7
|
+
works on the cached hash (ActiveOrient.database_classes)
|
6
8
|
=end
|
7
9
|
|
8
10
|
def classname name_or_class # :nodoc:
|
@@ -21,9 +23,11 @@ module ClassUtils
|
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
24
|
-
def allocate_class_in_ruby db_classname, &b
|
25
|
-
# retrieve the superclass recursively
|
26
26
|
|
27
|
+
# Updates ActiveOrient.database_classes
|
28
|
+
def allocate_class_in_ruby db_classname, &b
|
29
|
+
|
30
|
+
# first retrieve the superclass recursively
|
27
31
|
unless ActiveOrient.database_classes[ db_classname ].is_a? Class
|
28
32
|
|
29
33
|
s = get_db_superclass( db_classname )
|
@@ -46,10 +50,9 @@ module ClassUtils
|
|
46
50
|
elsif ActiveOrient::Model.namespace.send( :const_get, classname).ancestors.include?( ActiveOrient::Model )
|
47
51
|
ActiveOrient::Model.namespace.send( :const_get, classname)
|
48
52
|
else
|
49
|
-
|
50
|
-
logger.
|
51
|
-
|
52
|
-
t
|
53
|
+
logger.error{ "Unable to allocate class #{classname} in Namespace #{ActiveOrient::Model.namespace}"}
|
54
|
+
logger.error{ "Allocation took place with namespace ActiveOrient::Model" }
|
55
|
+
ActiveOrient::Model.send :const_set, classname, Class.new( superclass )
|
53
56
|
end
|
54
57
|
the_class.ref_name = db_classname
|
55
58
|
keep_the_dataset = block_given? ? yield( the_class ) : true
|
@@ -70,7 +73,7 @@ module ClassUtils
|
|
70
73
|
nil # return-value
|
71
74
|
end
|
72
75
|
else
|
73
|
-
# return
|
76
|
+
# return previously allocated ruby-class
|
74
77
|
ActiveOrient.database_classes[db_classname]
|
75
78
|
end
|
76
79
|
end
|
@@ -161,60 +164,18 @@ Creates one or more edge-classes and allocates the provided properties to each c
|
|
161
164
|
r = name.map{|n| create_class( n.to_s, properties: properties){ E } }
|
162
165
|
r.size == 1 ? r.pop : r # returns the created classes as array if multible classes are provided
|
163
166
|
end
|
164
|
-
=begin
|
165
|
-
|
166
|
-
creates a vertex
|
167
|
-
|
168
|
-
=end
|
169
|
-
def create_vertex( o_class, attributes:{} )
|
170
|
-
|
171
|
-
begin
|
172
|
-
response = execute(transaction: false, tolerated_error_code: /found duplicated key/) do
|
173
|
-
"CREATE VERTEX #{classname(o_class)} CONTENT #{attributes.to_orient.to_json}"
|
174
|
-
end
|
175
|
-
if response.is_a?(Array) && response.size == 1
|
176
|
-
response.pop # RETURN_VALUE
|
177
|
-
else
|
178
|
-
response # return value (the normal case)
|
179
|
-
end
|
180
|
-
rescue ArgumentError => e
|
181
|
-
puts "CreateVertex:ArgumentError "
|
182
|
-
puts e.inspect
|
183
|
-
end # begin
|
184
|
-
|
185
|
-
end
|
186
167
|
|
187
|
-
=begin
|
188
|
-
Deletes the specified vertices and unloads referenced edges from the cache
|
189
|
-
=end
|
190
|
-
def delete_vertex *vertex
|
191
|
-
create_command = -> do
|
192
|
-
{ type: "cmd",
|
193
|
-
language: 'sql',
|
194
|
-
command: "DELETE VERTEX #{vertex.map{|x| x.to_orient }.join(',')} "
|
195
|
-
}
|
196
|
-
end
|
197
|
-
|
198
|
-
vertex.each{|v| v.edges.each{| e | remove_record_from_hash e} }
|
199
|
-
execute{ create_command[] }
|
200
|
-
end
|
201
168
|
|
202
169
|
=begin
|
203
170
|
Deletes the specified edges and unloads referenced vertices from the cache
|
204
171
|
=end
|
205
172
|
def delete_edge *edge
|
206
|
-
create_command = -> do
|
207
|
-
{ type: "cmd",
|
208
|
-
language: 'sql',
|
209
|
-
command: "DELETE EDGE #{edge.map{|x| x.to_orient }.join(',')} "
|
210
|
-
}
|
211
|
-
end
|
212
173
|
|
213
174
|
edge.each do |r|
|
214
175
|
[r.in, r.out].each{| e | remove_record_from_hash e}
|
215
176
|
remove_record_from_hash r
|
216
177
|
end
|
217
|
-
|
178
|
+
execute{ "DELETE EDGE #{edge.map{|x| x.to_orient }.join(',')} "}
|
218
179
|
end
|
219
180
|
|
220
181
|
private
|
data/lib/database_utils.rb
CHANGED
@@ -13,7 +13,7 @@ if abstract: true is given, only basic classes (Abstact-Classes) are returend
|
|
13
13
|
## "ORid" dropped in V2.2
|
14
14
|
extended = ["OIdentity","ORole", "OUser", "OFunction", "_studio"]
|
15
15
|
v3 = ["OGeometryCollection", "OLineString", "OMultiLineString", "OMultiPoint", "OMultiPolygon",
|
16
|
-
"OPoint", "OPolygon", "ORectangle", "OShape"] ## added in Orentdb 3.0
|
16
|
+
"OPoint", "OPolygon", "ORectangle", "OShape", 'OSecurityPolicy'] ## added in Orentdb 3.0 and 3.1
|
17
17
|
if abstract
|
18
18
|
basic
|
19
19
|
else
|
@@ -33,7 +33,7 @@ To retrieve the class hierarchy from Objects avoid calling `ORD.classname (obj)`
|
|
33
33
|
=end
|
34
34
|
|
35
35
|
def class_hierarchy base_class: '', system_classes: nil
|
36
|
-
@actual_class_hash = get_classes('name', 'superClass') #if requery || @all_classes.blank?
|
36
|
+
# @actual_class_hash = get_classes('name', 'superClass') #if requery || @all_classes.blank?
|
37
37
|
fv = ->( s ) { @actual_class_hash.find_all{|x| x['superClass']== s}.map{|v| v['name']} }
|
38
38
|
fx = ->( v ) { fv[v.strip].map{|x| ar = fx[x]; ar.empty? ? x : [x, ar]} }
|
39
39
|
if system_classes.present?
|
@@ -45,13 +45,15 @@ To retrieve the class hierarchy from Objects avoid calling `ORD.classname (obj)`
|
|
45
45
|
|
46
46
|
|
47
47
|
=begin
|
48
|
-
|
48
|
+
|
49
|
+
Returns an array with all names of the classes of the database.
|
50
|
+
|
51
|
+
Reads the database-structure and updates the @actual_class_hash (used by class_hierachy and get_db_superclass )
|
49
52
|
|
50
|
-
Parameters: system_classes: false|true, requery: false|true
|
51
53
|
=end
|
52
54
|
|
53
|
-
def database_classes system_classes: nil
|
54
|
-
|
55
|
+
def database_classes system_classes: nil
|
56
|
+
@actual_class_hash = get_classes('name', 'superClass')
|
55
57
|
all_classes = get_classes('name').map(&:values).sort.flatten
|
56
58
|
all_user_classes = all_classes - system_classes()
|
57
59
|
|
@@ -65,8 +67,8 @@ Service-Method for Model#OrientdbClass
|
|
65
67
|
=end
|
66
68
|
|
67
69
|
def get_db_superclass name #:nodoc:
|
68
|
-
@actual_class_hash = get_classes( 'name', 'superClass') if @actual_class_hash.nil?
|
69
|
-
z= @actual_class_hash.find{|x,y| x['name'] == name.to_s }
|
70
|
+
# @actual_class_hash = get_classes( 'name', 'superClass') if @actual_class_hash.nil?
|
71
|
+
z= @actual_class_hash.find{|x,y| x['name'] == name.to_s }
|
70
72
|
z['superClass'] unless z.nil?
|
71
73
|
end
|
72
74
|
|
@@ -74,24 +76,23 @@ Service-Method for Model#OrientdbClass
|
|
74
76
|
preallocate classes reads any class from the @classes-Array and allocates adequat Ruby-Objects
|
75
77
|
=end
|
76
78
|
def preallocate_classes from_model_dir= nil # :nodoc:
|
77
|
-
# always scan for E+V model-
|
79
|
+
# always scan for E+V model-files and include
|
78
80
|
[E,V].each{|y| y.require_model_file(from_model_dir) }
|
79
81
|
|
80
82
|
ActiveOrient.database_classes.each do | db_name, the_class |
|
81
83
|
allocate_class_in_ruby( db_name ) do |detected_class|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end # block
|
84
|
+
# keep the class if it is already noted in database_classes
|
85
|
+
if [E,V].include?(detected_class) ||
|
86
|
+
ActiveOrient.database_classes.key( detected_class) ||
|
87
|
+
detected_class.require_model_file(from_model_dir) ||
|
88
|
+
ActiveOrient::Model.keep_models_without_file
|
89
|
+
true # return_value
|
90
|
+
else
|
91
|
+
logger.info{ "#{detected_class.name} --> Class NOT allocated"}
|
92
|
+
ActiveOrient.database_classes[ detected_class.ref_name ] = "no model file"
|
93
|
+
false # return_value
|
94
|
+
end
|
95
|
+
end # block
|
95
96
|
end # each iteration
|
96
97
|
end # def
|
97
98
|
|
data/lib/init.rb
CHANGED
@@ -37,7 +37,7 @@ returns the active OrientDB-Instance
|
|
37
37
|
def self.connect **defaults
|
38
38
|
define_namespace namespace: :object
|
39
39
|
ActiveOrient::OrientDB.configure_logger defaults[:logger]
|
40
|
-
ao = ActiveOrient::OrientDB.new defaults.merge preallocate: false
|
40
|
+
ao = ActiveOrient::OrientDB.new **(defaults.merge( preallocate: false))
|
41
41
|
ao.create_class 'E'
|
42
42
|
ao.create_class 'V'
|
43
43
|
ao # return client instance
|
@@ -45,7 +45,8 @@ returns the active OrientDB-Instance
|
|
45
45
|
=begin
|
46
46
|
Parameters:
|
47
47
|
yml: hash from config.yml ,
|
48
|
-
namespace: Class to use as Namespace
|
48
|
+
namespace: Class to use as Namespace, one of [ :self, :object, :active_orient ]
|
49
|
+
|
49
50
|
|
50
51
|
A custom Constant can be provided via Block
|
51
52
|
|
@@ -55,7 +56,7 @@ i.e.
|
|
55
56
|
#or
|
56
57
|
ActiveOrient.Init.define_namespace namespace: :self | :object | :active_orient
|
57
58
|
#or
|
58
|
-
module
|
59
|
+
module IB; end # first declare the Module-Const
|
59
60
|
# then assign to the namespace
|
60
61
|
ActiveOrient.Init.define_namespace { IB }
|
61
62
|
|