triannon 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/Rakefile +18 -6
- data/app/controllers/triannon/annotations_controller.rb +37 -0
- data/app/models/triannon/annotation.rb +115 -60
- data/app/models/triannon/annotation_ldp.rb +54 -0
- data/app/services/triannon/ldp_creator.rb +265 -0
- data/app/services/triannon/ldp_destroyer.rb +15 -0
- data/app/services/triannon/ldp_loader.rb +82 -0
- data/app/services/triannon/ldp_to_oa_mapper.rb +61 -0
- data/app/services/triannon/root_annotation_creator.rb +36 -0
- data/app/views/layouts/triannon/application.html.erb +2 -2
- data/app/views/triannon/annotations/_form.html.erb +1 -1
- data/app/views/triannon/annotations/show.html.erb +7 -7
- data/config/initializers/mime_types.rb +4 -0
- data/config/initializers/root_annotation_container.rb +8 -0
- data/config/triannon.yml +6 -0
- data/lib/generators/triannon/install_generator.rb +15 -0
- data/lib/rdf/triannon_vocab.rb +14 -0
- data/lib/triannon.rb +45 -1
- data/lib/triannon/oa_context_20130208.json +53 -0
- data/lib/triannon/version.rb +1 -1
- metadata +72 -7
- data/app/models/triannon/graph_validator.rb +0 -33
- data/db/migrate/20140912173046_create_triannon_annotations.rb +0 -9
@@ -0,0 +1,265 @@
|
|
1
|
+
|
2
|
+
module Triannon
|
3
|
+
|
4
|
+
# creates a new Annotation in the LDP server
|
5
|
+
class LdpCreator
|
6
|
+
|
7
|
+
# use LDP protocol to create the OpenAnnotation.Annotation in an RDF store
|
8
|
+
# @param [Triannon::Annotation] anno a Triannon::Annotation object, from which we use the graph
|
9
|
+
def self.create(anno)
|
10
|
+
# TODO: we should not get here if the Annotation object already has an id
|
11
|
+
result = Triannon::LdpCreator.new anno
|
12
|
+
result.create_base
|
13
|
+
|
14
|
+
bodies_solns = anno.graph.query([nil, RDF::OpenAnnotation.hasBody, nil])
|
15
|
+
if bodies_solns.size > 0
|
16
|
+
result.create_body_container
|
17
|
+
result.create_body_resources
|
18
|
+
end
|
19
|
+
|
20
|
+
targets_solns = anno.graph.query([nil, RDF::OpenAnnotation.hasTarget, nil])
|
21
|
+
# NOTE: Annotation is invalid if there are no target statements
|
22
|
+
if targets_solns.size > 0
|
23
|
+
result.create_target_container
|
24
|
+
result.create_target_resources
|
25
|
+
end
|
26
|
+
|
27
|
+
result.id
|
28
|
+
end
|
29
|
+
|
30
|
+
# given an RDF::Resource (an RDF::Node or RDF::URI), look for all the statements with that object
|
31
|
+
# as the subject, and recurse through the graph to find all descendant statements pertaining to the subject
|
32
|
+
# @param subject the RDF object to be used as the subject in the graph query. Should be an RDF::Node or RDF::URI
|
33
|
+
# @param [RDF::Graph] graph
|
34
|
+
# @return [Array[RDF::Statement]] all the triples with the given subject
|
35
|
+
def self.subject_statements(subject, graph)
|
36
|
+
result = []
|
37
|
+
graph.query([subject, nil, nil]).each { |stmt|
|
38
|
+
result << stmt
|
39
|
+
subject_statements(stmt.object, graph).each { |s| result << s }
|
40
|
+
}
|
41
|
+
result.uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_accessor :id
|
45
|
+
|
46
|
+
# @param [Triannon::Annotation] anno a Triannon::Annotation object
|
47
|
+
def initialize(anno)
|
48
|
+
@anno = anno
|
49
|
+
@base_uri = Triannon.config[:ldp_url]
|
50
|
+
end
|
51
|
+
|
52
|
+
# POSTS a ttl representation of the LDP Annotation container to the LDP store
|
53
|
+
def create_base
|
54
|
+
# TODO: we should error if the Annotation object already has an id
|
55
|
+
|
56
|
+
g = RDF::Graph.new
|
57
|
+
@anno.graph.each { |s|
|
58
|
+
g << s
|
59
|
+
}
|
60
|
+
|
61
|
+
# remove the hasBody statements and any other statements associated with them
|
62
|
+
bodies_stmts = g.query([nil, RDF::OpenAnnotation.hasBody, nil])
|
63
|
+
bodies_stmts.each { |has_body_stmt |
|
64
|
+
g.delete has_body_stmt
|
65
|
+
body_obj = has_body_stmt.object
|
66
|
+
Triannon::LdpCreator.subject_statements(body_obj, g).each { |s|
|
67
|
+
g.delete s
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
# remove the hasTarget statements and any other statements associated with them
|
72
|
+
targets_stmts = g.query([nil, RDF::OpenAnnotation.hasTarget, nil])
|
73
|
+
targets_stmts.each { |has_target_stmt |
|
74
|
+
g.delete has_target_stmt
|
75
|
+
target_obj = has_target_stmt.object
|
76
|
+
Triannon::LdpCreator.subject_statements(target_obj, g).each { |s|
|
77
|
+
g.delete s
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
# transform an outer blank node into a null relative URI
|
82
|
+
anno_stmts = g.query([nil, RDF.type, RDF::OpenAnnotation.Annotation])
|
83
|
+
anno_rdf_obj = anno_stmts.first.subject
|
84
|
+
if anno_rdf_obj.is_a?(RDF::Node)
|
85
|
+
# we need to use the null relative URI representation of blank nodes to write to LDP
|
86
|
+
anno_subject = RDF::URI.new
|
87
|
+
else # it's already a URI
|
88
|
+
anno_subject = anno_rdf_obj
|
89
|
+
end
|
90
|
+
Triannon::LdpCreator.subject_statements(anno_rdf_obj, g).each { |s|
|
91
|
+
if s.subject == anno_rdf_obj && anno_subject != anno_rdf_obj
|
92
|
+
g << RDF::Statement({:subject => anno_subject,
|
93
|
+
:predicate => s.predicate,
|
94
|
+
:object => s.object})
|
95
|
+
g.delete s
|
96
|
+
end
|
97
|
+
}
|
98
|
+
|
99
|
+
@id = create_resource g.to_ttl
|
100
|
+
end
|
101
|
+
|
102
|
+
# creates the LDP container for any and all bodies for this annotation
|
103
|
+
def create_body_container
|
104
|
+
create_direct_container RDF::OpenAnnotation.hasBody
|
105
|
+
end
|
106
|
+
|
107
|
+
# creates the LDP container for any and all targets for this annotation
|
108
|
+
def create_target_container
|
109
|
+
create_direct_container RDF::OpenAnnotation.hasTarget
|
110
|
+
end
|
111
|
+
|
112
|
+
# create the body resources inside the (already created) body container
|
113
|
+
def create_body_resources
|
114
|
+
bodies_solns = @anno.graph.query([nil, RDF::OpenAnnotation.hasBody, nil])
|
115
|
+
body_ids = []
|
116
|
+
bodies_solns.each { |has_body_stmt |
|
117
|
+
graph_for_resource = RDF::Graph.new
|
118
|
+
body_obj = has_body_stmt.object
|
119
|
+
if body_obj.is_a?(RDF::Node)
|
120
|
+
# we need to use the null relative URI representation of blank nodes to write to LDP
|
121
|
+
body_subject = RDF::URI.new
|
122
|
+
else
|
123
|
+
# it's already a URI, but we need to use the null relative URI representation so we can
|
124
|
+
# write out as a Triannon:externalRef property with the URL, and any addl props too.
|
125
|
+
if body_obj.to_str
|
126
|
+
body_subject = RDF::URI.new
|
127
|
+
graph_for_resource << RDF::Statement({:subject => body_subject,
|
128
|
+
:predicate => RDF::Triannon.externalReference,
|
129
|
+
:object => RDF::URI.new(body_obj.to_str)})
|
130
|
+
addl_stmts = @anno.graph.query([body_obj, nil, nil])
|
131
|
+
addl_stmts.each { |s|
|
132
|
+
graph_for_resource << RDF::Statement({:subject => body_subject,
|
133
|
+
:predicate => s.predicate,
|
134
|
+
:object => s.object})
|
135
|
+
}
|
136
|
+
else # it's already a null relative URI
|
137
|
+
body_subject = body_obj
|
138
|
+
end
|
139
|
+
end
|
140
|
+
# add statements with body_obj as the subject
|
141
|
+
Triannon::LdpCreator.subject_statements(body_obj, @anno.graph).each { |s|
|
142
|
+
if s.subject == body_obj
|
143
|
+
graph_for_resource << RDF::Statement({:subject => body_subject,
|
144
|
+
:predicate => s.predicate,
|
145
|
+
:object => s.object})
|
146
|
+
else
|
147
|
+
graph_for_resource << s
|
148
|
+
end
|
149
|
+
}
|
150
|
+
body_ids << create_resource(graph_for_resource.to_ttl, "#{@id}/b")
|
151
|
+
}
|
152
|
+
body_ids
|
153
|
+
end
|
154
|
+
|
155
|
+
# create the target resources inside the (already created) target container
|
156
|
+
def create_target_resources
|
157
|
+
target_solns = @anno.graph.query([nil, RDF::OpenAnnotation.hasTarget, nil])
|
158
|
+
target_ids = []
|
159
|
+
target_solns.each { |has_target_stmt |
|
160
|
+
graph_for_resource = RDF::Graph.new
|
161
|
+
target_obj = has_target_stmt.object
|
162
|
+
if target_obj.is_a?(RDF::Node)
|
163
|
+
# we need to use the null relative URI representation of blank nodes to write to LDP
|
164
|
+
target_subject = RDF::URI.new
|
165
|
+
else
|
166
|
+
# it's already a URI, but we need to use the null relative URI representation so we can
|
167
|
+
# write out as a Triannon:externalRef property with the URL, and any addl props too.
|
168
|
+
if target_obj.to_str
|
169
|
+
target_subject = RDF::URI.new
|
170
|
+
graph_for_resource << RDF::Statement({:subject => target_subject,
|
171
|
+
:predicate => RDF::Triannon.externalReference,
|
172
|
+
:object => RDF::URI.new(target_obj.to_str)})
|
173
|
+
addl_stmts = @anno.graph.query([target_obj, nil, nil])
|
174
|
+
addl_stmts.each { |s|
|
175
|
+
graph_for_resource << RDF::Statement({:subject => target_subject,
|
176
|
+
:predicate => s.predicate,
|
177
|
+
:object => s.object})
|
178
|
+
}
|
179
|
+
else # it's already a null relative URI
|
180
|
+
target_subject = target_obj
|
181
|
+
end
|
182
|
+
end
|
183
|
+
# add statements with target_obj as the subject
|
184
|
+
orig_source_objects = [] # the object URI nodes from targetObject, OA.hasSource, (uri) statements
|
185
|
+
Triannon::LdpCreator.subject_statements(target_obj, @anno.graph).each { |s|
|
186
|
+
if s.subject == target_obj
|
187
|
+
# deal with external references in hasSource statements (i.e. targetObject, OA.hasSource, (url) )
|
188
|
+
if s.predicate == RDF::OpenAnnotation.hasSource && s.object.is_a?(RDF::URI) && s.object.to_str
|
189
|
+
# we need to represent the source URL as an externalReference - use a hash URI
|
190
|
+
source_object = RDF::URI.new("#source")
|
191
|
+
orig_source_objects << s.object
|
192
|
+
graph_for_resource << RDF::Statement({:subject => source_object,
|
193
|
+
:predicate => RDF::Triannon.externalReference,
|
194
|
+
:object => RDF::URI.new(s.object.to_str)})
|
195
|
+
# and all of the source URL's addl props
|
196
|
+
Triannon::LdpCreator.subject_statements(s.object, @anno.graph).each { |ss|
|
197
|
+
if ss.subject == s.object
|
198
|
+
graph_for_resource << RDF::Statement({:subject => source_object,
|
199
|
+
:predicate => ss.predicate,
|
200
|
+
:object => ss.object})
|
201
|
+
else
|
202
|
+
graph_for_resource << ss
|
203
|
+
end
|
204
|
+
}
|
205
|
+
# add the targetObj, OA.hasSource, (hash URI representation) to graph
|
206
|
+
graph_for_resource << RDF::Statement({:subject => target_subject,
|
207
|
+
:predicate => s.predicate,
|
208
|
+
:object => source_object})
|
209
|
+
else
|
210
|
+
graph_for_resource << RDF::Statement({:subject => target_subject,
|
211
|
+
:predicate => s.predicate,
|
212
|
+
:object => s.object})
|
213
|
+
end
|
214
|
+
else
|
215
|
+
graph_for_resource << s
|
216
|
+
end
|
217
|
+
}
|
218
|
+
# make sure the graph we will write contains no extraneous statements about source URI
|
219
|
+
# now represented as hash URI (#source)
|
220
|
+
orig_source_objects.each { |rdf_uri_node|
|
221
|
+
Triannon::LdpCreator.subject_statements(rdf_uri_node, graph_for_resource).each { |s|
|
222
|
+
graph_for_resource.delete(s)
|
223
|
+
}
|
224
|
+
}
|
225
|
+
target_ids << create_resource(graph_for_resource.to_ttl, "#{@id}/t")
|
226
|
+
}
|
227
|
+
target_ids
|
228
|
+
end
|
229
|
+
|
230
|
+
def conn
|
231
|
+
@c ||= Faraday.new @base_uri
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
def create_resource body, url = nil
|
236
|
+
response = conn.post do |req|
|
237
|
+
req.url url if url
|
238
|
+
req.headers['Content-Type'] = 'application/x-turtle'
|
239
|
+
req.body = body
|
240
|
+
end
|
241
|
+
new_url = response.headers['Location'] ? response.headers['Location'] : response.headers['location']
|
242
|
+
new_url.split('/').last if new_url
|
243
|
+
end
|
244
|
+
|
245
|
+
# Creates an empty LDP DirectContainer in LDP Storage that is a member of the base container and has the memberRelation per the oa_vocab_term
|
246
|
+
# The id of the created containter will be (base container id)b if hasBody or (base container id)/t if hasTarget
|
247
|
+
# @param [RDF::Vocabulary::Term] oa_vocab_term RDF::OpenAnnotation.hasTarget or RDF::OpenAnnotation.hasBody
|
248
|
+
def create_direct_container oa_vocab_term
|
249
|
+
null_rel_uri = RDF::URI.new
|
250
|
+
g = RDF::Graph.new
|
251
|
+
g << [null_rel_uri, RDF.type, RDF::LDP.DirectContainer]
|
252
|
+
g << [null_rel_uri, RDF::LDP.hasMemberRelation, oa_vocab_term]
|
253
|
+
g << [null_rel_uri, RDF::LDP.membershipResource, RDF::URI.new("#{@base_uri}/#{id}")]
|
254
|
+
|
255
|
+
response = conn.post do |req|
|
256
|
+
req.url "#{id}"
|
257
|
+
req.headers['Content-Type'] = 'application/x-turtle'
|
258
|
+
# OA vocab relationships all of form "hasXXX"
|
259
|
+
req.headers['Slug'] = oa_vocab_term.fragment.slice(3).downcase
|
260
|
+
req.body = g.to_ttl
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
module Triannon
|
3
|
+
|
4
|
+
class LdpDestroyer
|
5
|
+
|
6
|
+
def self.destroy key
|
7
|
+
conn = Faraday.new Triannon.config[:ldp_url]
|
8
|
+
|
9
|
+
resp = conn.delete { |req| req.url key }
|
10
|
+
if resp.status != 204
|
11
|
+
raise "Unable to delete Annotation: #{key}\nResponse Status: #{resp.status}\nResponse Body: #{resp.body}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
|
2
|
+
module Triannon
|
3
|
+
|
4
|
+
# Loads an existing Annotation from the LDP server
|
5
|
+
class LdpLoader
|
6
|
+
|
7
|
+
def self.load key
|
8
|
+
l = Triannon::LdpLoader.new key
|
9
|
+
l.load_annotation
|
10
|
+
l.load_body
|
11
|
+
l.load_target
|
12
|
+
|
13
|
+
oa_graph = Triannon::LdpToOaMapper.ldp_to_oa l.annotation
|
14
|
+
oa_graph
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_all
|
18
|
+
l = Triannon::LdpLoader.new
|
19
|
+
l.find_all
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :annotation
|
23
|
+
|
24
|
+
def initialize key = nil
|
25
|
+
@key = key
|
26
|
+
@base_uri = Triannon.config[:ldp_url]
|
27
|
+
@annotation = Triannon::AnnotationLdp.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_annotation
|
31
|
+
@annotation.load_data_into_graph get_ttl @key
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_body
|
35
|
+
@annotation.body_uris.each { |body_uri|
|
36
|
+
sub_path = body_uri.to_s.split(@base_uri + '/').last
|
37
|
+
@annotation.load_data_into_graph get_ttl sub_path
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_target
|
42
|
+
@annotation.target_uris.each { |target_uri|
|
43
|
+
sub_path = target_uri.to_s.split(@base_uri + '/').last
|
44
|
+
@annotation.load_data_into_graph get_ttl sub_path
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<Triannon::Annotation>] an array of Triannon::Annotation objects with just the id set. Enough info to build the index page
|
49
|
+
def find_all
|
50
|
+
root_ttl = get_ttl
|
51
|
+
objs = []
|
52
|
+
|
53
|
+
g = RDF::Graph.new
|
54
|
+
g.from_ttl root_ttl
|
55
|
+
root_uri = RDF::URI.new @base_uri
|
56
|
+
results = g.query [root_uri, RDF::LDP.contains, nil]
|
57
|
+
results.each do |stmt|
|
58
|
+
id = stmt.object.to_s.split('/').last
|
59
|
+
objs << Triannon::Annotation.new(:id => id)
|
60
|
+
end
|
61
|
+
|
62
|
+
objs
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def get_ttl sub_path = nil
|
68
|
+
resp = conn.get do |req|
|
69
|
+
req.url "#{sub_path}" if sub_path
|
70
|
+
req.headers['Accept'] = 'text/turtle'
|
71
|
+
end
|
72
|
+
resp.body
|
73
|
+
end
|
74
|
+
|
75
|
+
def conn
|
76
|
+
@c ||= Faraday.new @base_uri
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Triannon
|
2
|
+
class LdpToOaMapper
|
3
|
+
|
4
|
+
# maps an AnnotationLdp to an OA RDF::Graph
|
5
|
+
def self.ldp_to_oa ldp_anno
|
6
|
+
mapper = Triannon::LdpToOaMapper.new ldp_anno
|
7
|
+
mapper.extract_base
|
8
|
+
mapper.extract_body
|
9
|
+
mapper.extract_target
|
10
|
+
mapper.oa_graph
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :id, :oa_graph
|
14
|
+
|
15
|
+
def initialize ldp_anno
|
16
|
+
@ldp = ldp_anno
|
17
|
+
@oa_graph = RDF::Graph.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_base
|
21
|
+
@ldp.stripped_graph.each_statement do |stmnt|
|
22
|
+
if stmnt.predicate == RDF.type && stmnt.object == RDF::OpenAnnotation.Annotation
|
23
|
+
@id = stmnt.subject.to_s.split('/').last
|
24
|
+
@root_uri = RDF::URI.new(Triannon.config[:triannon_base_url] + "/#{@id}")
|
25
|
+
@oa_graph << [@root_uri, RDF.type, RDF::OpenAnnotation.Annotation]
|
26
|
+
|
27
|
+
elsif stmnt.predicate == RDF::OpenAnnotation.motivatedBy
|
28
|
+
@oa_graph << [@root_uri, stmnt.predicate, stmnt.object]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_body
|
34
|
+
@ldp.body_uris.each { |body_uri|
|
35
|
+
res = @ldp.stripped_graph.query [body_uri, RDF.type, RDF::Content.ContentAsText]
|
36
|
+
if res.count > 0 # TODO raise if this fails?
|
37
|
+
body_node = RDF::Node.new
|
38
|
+
@oa_graph << [@root_uri, RDF::OpenAnnotation.hasBody, body_node]
|
39
|
+
@oa_graph << [body_node, RDF.type, RDF::Content.ContentAsText]
|
40
|
+
@oa_graph << [body_node, RDF.type, RDF::DCMIType.Text]
|
41
|
+
res_chars = @ldp.stripped_graph.query [body_uri, RDF::Content.chars, nil]
|
42
|
+
if res_chars.count > 0
|
43
|
+
@oa_graph << [body_node, RDF::Content.chars, res_chars.first.object]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_target
|
50
|
+
@ldp.target_uris.each { |target_uri|
|
51
|
+
res = @ldp.stripped_graph.query [target_uri, RDF::Triannon.externalReference, nil]
|
52
|
+
if res.count > 0
|
53
|
+
ext_uri = res.first.object
|
54
|
+
@oa_graph << [@root_uri, RDF::OpenAnnotation.hasTarget, ext_uri]
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Triannon
|
2
|
+
|
3
|
+
class RootAnnotationCreator
|
4
|
+
|
5
|
+
# Creates an LDP Container to hold all the annotations
|
6
|
+
# Called from config/initializers/root_annotation_container.rb during app bootup
|
7
|
+
# @return [Boolean] true if the root container was created, false if the container already exists or if there were issues
|
8
|
+
def self.create
|
9
|
+
conn = Faraday.new :url => Triannon.config[:ldp_url]
|
10
|
+
resp = conn.head
|
11
|
+
unless resp.status == 404 || resp.status == 410
|
12
|
+
Rails.logger.info "Root annotation resource already created."
|
13
|
+
return false
|
14
|
+
end
|
15
|
+
|
16
|
+
uri = RDF::URI.new Triannon.config[:ldp_url]
|
17
|
+
conn = Faraday.new :url => uri.parent.to_s
|
18
|
+
slug = uri.to_s.split('/').last
|
19
|
+
|
20
|
+
resp = conn.post do |req|
|
21
|
+
req.headers['Content-Type'] = 'text/turtle'
|
22
|
+
req.headers['Slug'] = slug
|
23
|
+
end
|
24
|
+
|
25
|
+
if resp.status == 201
|
26
|
+
Rails.logger.info "Created root annotation container #{Triannon.config[:ldp_url]}"
|
27
|
+
return true
|
28
|
+
else
|
29
|
+
Rails.logger.warn "Unable to create root annotation container #{Triannon.config[:ldp_url]}"
|
30
|
+
return false
|
31
|
+
# TODO raise an exception if we get here?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|