infopark_cloud_connector 6.8.3.115.227021242 → 6.8.3.174.51542603
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rails_connector/basic_obj.rb +1 -2
- data/lib/rails_connector/blob.rb +19 -52
- data/lib/rails_connector/cache_middleware.rb +0 -1
- data/lib/rails_connector/cms_backend.rb +179 -17
- data/lib/rails_connector/model_identity.rb +13 -0
- data/lib/rails_connector/workspace.rb +2 -11
- metadata +4 -49
- data/lib/rails_connector/chain.rb +0 -175
- data/lib/rails_connector/cms_base_model.rb +0 -55
- data/lib/rails_connector/content_cache.rb +0 -23
- data/lib/rails_connector/dict_storage.rb +0 -66
- data/lib/rails_connector/dynamo_cms_backend.rb +0 -136
- data/lib/rails_connector/obj_data_from_database.rb +0 -109
- data/lib/rails_connector/path_conversion.rb +0 -21
- data/lib/rails_connector/permission.rb +0 -39
- data/lib/rails_connector/revision.rb +0 -75
- data/lib/rails_connector/s3_blob.rb +0 -89
- data/lib/rails_connector/service_blob.rb +0 -48
- data/lib/rails_connector/service_cms_backend.rb +0 -194
- data/lib/rails_connector/version.rb +0 -38
- data/lib/rails_connector/workspace_data_from_database.rb +0 -19
@@ -1,109 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
class ObjDataFromDatabase < ObjData
|
4
|
-
|
5
|
-
def initialize(data, revision)
|
6
|
-
@rtc_ref = data["rtc_ref"]
|
7
|
-
@values = data["values"]
|
8
|
-
@ext_ref = data["ext_ref"]
|
9
|
-
@revision = revision
|
10
|
-
end
|
11
|
-
|
12
|
-
def value_and_type_of(attribute_name)
|
13
|
-
if internal_attribute?(attribute_name)
|
14
|
-
attribute_type = type_of_internal(attribute_name)
|
15
|
-
|
16
|
-
attribute_value = read_value(attribute_name)
|
17
|
-
if attribute_name == '_text_links'
|
18
|
-
attribute_value = attribute_value && attribute_value.values
|
19
|
-
elsif attribute_name == '_path'
|
20
|
-
attribute_value = PathConversion.path_from_list(attribute_value)
|
21
|
-
end
|
22
|
-
else
|
23
|
-
attribute_type = current_attribute_type(attribute_name)
|
24
|
-
if legacy_attribute_type(attribute_name) == attribute_type
|
25
|
-
attribute_value = read_value(attribute_name)
|
26
|
-
else
|
27
|
-
attribute_value = default_attribute_value(attribute_type)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
[attribute_value, attribute_type]
|
32
|
-
end
|
33
|
-
|
34
|
-
def has_custom_attribute?(name)
|
35
|
-
obj_class_attributes.include?(name)
|
36
|
-
end
|
37
|
-
|
38
|
-
def all_custom_attributes
|
39
|
-
obj_class_attributes
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
attr_reader :values, :rtc_ref, :revision
|
45
|
-
|
46
|
-
def current_attribute_type(name)
|
47
|
-
revision.attributes[name]["type"]
|
48
|
-
end
|
49
|
-
|
50
|
-
def obj_class_attributes
|
51
|
-
@obj_class_attributes ||=
|
52
|
-
begin
|
53
|
-
name = values["_obj_class"]
|
54
|
-
if data = revision.obj_classes[name]
|
55
|
-
Set.new(data["attributes"]) || Set.new
|
56
|
-
else
|
57
|
-
raise "Could not find ObjClass with name #{name} in Revision #{revision.id}!"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def read_value(attribute_name)
|
63
|
-
return values[attribute_name] if values.key?(attribute_name)
|
64
|
-
if @ext_ref && (attribute_name == "_text_links" || attribute_name[0] != ?_)
|
65
|
-
extend_values_with_dict_storage_values
|
66
|
-
end
|
67
|
-
values[attribute_name]
|
68
|
-
end
|
69
|
-
|
70
|
-
def extend_values_with_dict_storage_values
|
71
|
-
return unless @ext_ref
|
72
|
-
|
73
|
-
# may raise Kvom::Storage::NotFound
|
74
|
-
ext_values = DictStorage.get(@ext_ref)
|
75
|
-
values.reverse_merge!(ext_values)
|
76
|
-
@ext_ref = nil
|
77
|
-
end
|
78
|
-
|
79
|
-
SPECIAL_INTERNAL_ATTRIBUTES = Set.new(%w[title body blob])
|
80
|
-
|
81
|
-
def internal_attribute?(attribute_name)
|
82
|
-
?_ == attribute_name[0] || SPECIAL_INTERNAL_ATTRIBUTES.include?(attribute_name)
|
83
|
-
end
|
84
|
-
|
85
|
-
def legacy_attribute_type(name)
|
86
|
-
if attr_def = legacy_attributes[name]
|
87
|
-
attr_def["type"]
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def legacy_attributes
|
92
|
-
@attributes ||=
|
93
|
-
begin
|
94
|
-
rtc = DictStorage.get(rtc_ref)
|
95
|
-
if rtc['obj_classes'] && rtc['attributes'] && oc = rtc['obj_classes'][values['_obj_class']]
|
96
|
-
rtc['attributes'].inject({}) do |attrs, (name, value)|
|
97
|
-
attrs[name] = value if oc['attributes'] && oc['attributes'].include?(name)
|
98
|
-
attrs
|
99
|
-
end
|
100
|
-
else
|
101
|
-
{}
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
end
|
109
|
-
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
module PathConversion
|
4
|
-
|
5
|
-
def self.path_from_list(components)
|
6
|
-
if components
|
7
|
-
"/#{components.join("/")}"
|
8
|
-
else
|
9
|
-
"/"
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def path_from_list(components)
|
16
|
-
PathConversion.path_from_list(components)
|
17
|
-
end
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
# The permissions assigned to an Obj.
|
4
|
-
class Permission < CmsBaseModel
|
5
|
-
# Returns the Array of the names of the user groups to which read permission has been granted.
|
6
|
-
def self.read
|
7
|
-
user_groups_for_permission_type(1)
|
8
|
-
end
|
9
|
-
|
10
|
-
# Returns the Array of the names of the user groups to which write permission has been granted
|
11
|
-
def self.write
|
12
|
-
user_groups_for_permission_type(2)
|
13
|
-
end
|
14
|
-
|
15
|
-
# Returns the Array of the names of the user groups to which root permission has been granted
|
16
|
-
def self.root
|
17
|
-
user_groups_for_permission_type(3)
|
18
|
-
end
|
19
|
-
|
20
|
-
# Returns the Array of the names of the user groups to which the children creation permission has been granted.
|
21
|
-
def self.create_children
|
22
|
-
user_groups_for_permission_type(4)
|
23
|
-
end
|
24
|
-
|
25
|
-
# Returns the Array of the names of the user groups to which live read permission has been granted.
|
26
|
-
def self.live
|
27
|
-
user_groups_for_permission_type(5)
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def self.user_groups_for_permission_type(permission_type)
|
33
|
-
# Field name "user_login" is a legacy from CM tables.
|
34
|
-
# Actually the field contains the user group.
|
35
|
-
select(:user_login).where(:permission_type => permission_type).map(&:user_login)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
@@ -1,75 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
class Revision < CmsBaseModel
|
4
|
-
self.key_prefix = "rev"
|
5
|
-
|
6
|
-
property :generation
|
7
|
-
property :base_revision_id
|
8
|
-
property :title
|
9
|
-
property :content_cache_id
|
10
|
-
property :editable
|
11
|
-
|
12
|
-
def self.current=(revision_or_proc)
|
13
|
-
raise "Do not use Revision.current=, use Workspace.current= instead."
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.current
|
17
|
-
raise "Do not use Revision.current, use Workspace.current instead."
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.find_by_label(label_name)
|
21
|
-
workspace = Workspace.find(label_name)
|
22
|
-
find(workspace.revision_id)
|
23
|
-
end
|
24
|
-
|
25
|
-
def chain
|
26
|
-
@chain ||= Chain.build_for(self, content_cache)
|
27
|
-
end
|
28
|
-
|
29
|
-
def invalidate_chain
|
30
|
-
@chain = nil
|
31
|
-
end
|
32
|
-
|
33
|
-
# returns the base revision or nil for an initial revision
|
34
|
-
def base_revision
|
35
|
-
@base_revision ||= base_revision_id ? Revision.find(base_revision_id) : nil
|
36
|
-
end
|
37
|
-
|
38
|
-
# returns the content cache to be used with this revision or nil if not available
|
39
|
-
def content_cache
|
40
|
-
if content_cache_id
|
41
|
-
if @content_cache && content_cache_id == @content_cache.id
|
42
|
-
@content_cache
|
43
|
-
else
|
44
|
-
@content_cache = ContentCache.find_by_id(content_cache_id)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def inspect
|
50
|
-
"<#{self.class} id=\"#{id}\" title=\"#{title}\">"
|
51
|
-
end
|
52
|
-
|
53
|
-
def obj_classes
|
54
|
-
rtc['obj_classes']
|
55
|
-
end
|
56
|
-
|
57
|
-
def attributes
|
58
|
-
rtc['attributes']
|
59
|
-
end
|
60
|
-
|
61
|
-
def rtc_id
|
62
|
-
rtc_ref['id']
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def rtc
|
68
|
-
@rtc ||= DictStorage.get(rtc_ref)
|
69
|
-
end
|
70
|
-
|
71
|
-
def rtc_ref
|
72
|
-
read_attribute('rtc_ref')
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
@@ -1,89 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
class S3Blob
|
4
|
-
class << self
|
5
|
-
|
6
|
-
def s3_objects
|
7
|
-
@s3_objects ||= bucket.objects
|
8
|
-
end
|
9
|
-
|
10
|
-
def bucket_url
|
11
|
-
@cached_bucket_url ||= bucket.url
|
12
|
-
end
|
13
|
-
|
14
|
-
def configure(spec)
|
15
|
-
@spec = spec.symbolize_keys
|
16
|
-
|
17
|
-
# invalidate cache
|
18
|
-
@cached_bucket_url = nil
|
19
|
-
@s3_objects = nil
|
20
|
-
end
|
21
|
-
|
22
|
-
def find(id)
|
23
|
-
new(id)
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
attr_reader :spec
|
29
|
-
|
30
|
-
def s3_api
|
31
|
-
AWS::S3.new(
|
32
|
-
:access_key_id => access_key_id,
|
33
|
-
:secret_access_key => secret_access_key
|
34
|
-
)
|
35
|
-
end
|
36
|
-
|
37
|
-
def bucket
|
38
|
-
s3_api.buckets[bucket_name]
|
39
|
-
end
|
40
|
-
|
41
|
-
def bucket_name
|
42
|
-
spec[:bucket_name]
|
43
|
-
end
|
44
|
-
|
45
|
-
def access_key_id
|
46
|
-
spec[:access_key_id]
|
47
|
-
end
|
48
|
-
|
49
|
-
def secret_access_key
|
50
|
-
spec[:secret_access_key]
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
attr_reader :id
|
56
|
-
|
57
|
-
def initialize(id)
|
58
|
-
@id = id
|
59
|
-
end
|
60
|
-
|
61
|
-
def s3_object
|
62
|
-
@s3_object ||= self.class.s3_objects[id]
|
63
|
-
end
|
64
|
-
|
65
|
-
def url
|
66
|
-
# the 'official' way to calculate an s3 url is as follows:
|
67
|
-
# s3_object.public_url.to_s
|
68
|
-
#
|
69
|
-
# this is about 25x faster:
|
70
|
-
# assumption: 'id' does not contain any characters that need escaping.
|
71
|
-
# the assumptions is valid for cms blobs.
|
72
|
-
"#{self.class.bucket_url}#{@id}"
|
73
|
-
end
|
74
|
-
|
75
|
-
def meta_url
|
76
|
-
url
|
77
|
-
end
|
78
|
-
|
79
|
-
def content_type
|
80
|
-
s3_object.content_type
|
81
|
-
end
|
82
|
-
|
83
|
-
def length
|
84
|
-
s3_object.content_length
|
85
|
-
end
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
end # module RailsConnector
|
@@ -1,48 +0,0 @@
|
|
1
|
-
module RailsConnector
|
2
|
-
|
3
|
-
# TODO Caching:
|
4
|
-
# unlimited urls: ewig
|
5
|
-
# limited urls: zeitlimit - x (x fuer mindestverwendbarkeitszeit)
|
6
|
-
class ServiceBlob
|
7
|
-
|
8
|
-
class << self
|
9
|
-
|
10
|
-
def configure(config)
|
11
|
-
end
|
12
|
-
|
13
|
-
def find(id)
|
14
|
-
new(id)
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :id
|
20
|
-
|
21
|
-
def initialize(id)
|
22
|
-
@id = id
|
23
|
-
end
|
24
|
-
|
25
|
-
def url
|
26
|
-
raw_data["url"]
|
27
|
-
end
|
28
|
-
|
29
|
-
def content_type
|
30
|
-
meta_data[:content_type]
|
31
|
-
end
|
32
|
-
|
33
|
-
def length
|
34
|
-
meta_data[:content_length].to_i
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def raw_data
|
40
|
-
@raw_data ||= CmsBackend.find_blob_data_by_id(id)
|
41
|
-
end
|
42
|
-
|
43
|
-
def meta_data
|
44
|
-
@meta_data ||= RestClient.head(raw_data["meta_url"]).headers
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
@@ -1,194 +0,0 @@
|
|
1
|
-
require 'rest_client'
|
2
|
-
|
3
|
-
module RailsConnector
|
4
|
-
|
5
|
-
class ContentServiceObjQueries
|
6
|
-
def initialize(queries)
|
7
|
-
@queries = queries
|
8
|
-
@open_queries = queries.dup
|
9
|
-
@results = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def open_queries
|
13
|
-
@open_queries[0..99]
|
14
|
-
end
|
15
|
-
|
16
|
-
def handle_response(response)
|
17
|
-
objs = {}
|
18
|
-
response["objs"].each do |obj|
|
19
|
-
objs[obj["_id"].first] = obj
|
20
|
-
end
|
21
|
-
|
22
|
-
queries_to_delete = []
|
23
|
-
response["results"].each_with_index do |response, i|
|
24
|
-
query = @open_queries[i]
|
25
|
-
if response["continuation_handle"]
|
26
|
-
query[:continuation_handle] = response["continuation_handle"]
|
27
|
-
else
|
28
|
-
queries_to_delete << i
|
29
|
-
end
|
30
|
-
result = (@results[query.__id__] ||= [])
|
31
|
-
response["refs"].each do |obj_ref|
|
32
|
-
id = obj_ref["id"]
|
33
|
-
# TODO fetch missing ObjData from Service
|
34
|
-
result << (objs[id] or raise "Data for Obj with id #{id} missing!")
|
35
|
-
end
|
36
|
-
end
|
37
|
-
queries_to_delete.reverse_each {|i| @open_queries.delete_at(i) }
|
38
|
-
end
|
39
|
-
|
40
|
-
def results
|
41
|
-
@queries.map {|query| @results[query.__id__] || [] }
|
42
|
-
end
|
43
|
-
|
44
|
-
def finished?
|
45
|
-
open_queries.empty?
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# connects the cloud connector to the connector service
|
50
|
-
class ServiceCmsBackend < CmsBackend
|
51
|
-
BLOB_DATA_CACHE_PREFIX = 'blob_data'.freeze
|
52
|
-
VALID_INDEX_NAMES = %w[id path ppath permalink].freeze
|
53
|
-
|
54
|
-
def initialize
|
55
|
-
@query_counter = 0
|
56
|
-
end
|
57
|
-
|
58
|
-
def begin_caching
|
59
|
-
@caching = true
|
60
|
-
end
|
61
|
-
|
62
|
-
def end_caching
|
63
|
-
CmsCacheStorage.cache.clear
|
64
|
-
@caching = false
|
65
|
-
end
|
66
|
-
|
67
|
-
def caching?
|
68
|
-
!!@caching
|
69
|
-
end
|
70
|
-
|
71
|
-
def query_counter
|
72
|
-
@query_counter
|
73
|
-
end
|
74
|
-
|
75
|
-
def find_workspace_data_by_id(id)
|
76
|
-
from_content_state_id = ContentStateCaching.find_content_state_id(id)
|
77
|
-
request_params = {:workspace_id => id}
|
78
|
-
request_params[:content_state_id] = from_content_state_id if from_content_state_id
|
79
|
-
raw_data = ContentService.query('workspaces/query', request_params)
|
80
|
-
if raw_workspace_data = raw_data['workspace']
|
81
|
-
workspace_data = WorkspaceDataFromService.new(raw_workspace_data)
|
82
|
-
if from_content_state_id != workspace_data.content_state_id
|
83
|
-
ContentStateCaching.store_content_state(workspace_data)
|
84
|
-
ContentStateCaching.store_content_state_id(workspace_data.id,
|
85
|
-
workspace_data.content_state_id)
|
86
|
-
end
|
87
|
-
workspace_data
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def find_obj_data_by(workspace_data, index, keys)
|
92
|
-
index = index.to_s
|
93
|
-
assert_valid_index_name(index)
|
94
|
-
raw_data = find_raw_data_from_cache_or_database_by(workspace_data, index, keys)
|
95
|
-
raw_data.map do |raw_list|
|
96
|
-
raw_list.map do |raw_obj_data|
|
97
|
-
ObjDataFromService.new(raw_obj_data)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def find_blob_data_by_id(id)
|
103
|
-
cache_key = "#{BLOB_DATA_CACHE_PREFIX}/#{id}"
|
104
|
-
if data_from_cache = CmsCacheStorage.cache.read(cache_key)
|
105
|
-
data_from_cache
|
106
|
-
else
|
107
|
-
data_from_database = find_blob_data_from_database_by(id)
|
108
|
-
if maxage = data_from_database['maxage']
|
109
|
-
CmsCacheStorage.cache.write(cache_key, data_from_database, :expires_in => maxage)
|
110
|
-
end
|
111
|
-
data_from_database
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
private
|
116
|
-
|
117
|
-
def find_raw_data_from_cache_or_database_by(workspace_data, index, keys)
|
118
|
-
keys_from_database = []
|
119
|
-
# load results from cache
|
120
|
-
results_from_cache = keys.map do |key|
|
121
|
-
find_raw_data_from_cache_by(workspace_data, index, key).tap do |objs|
|
122
|
-
keys_from_database << key unless objs
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# load cache misses from database and store them in cache
|
127
|
-
results_from_database =
|
128
|
-
find_raw_data_from_database_by(workspace_data, index, keys_from_database)
|
129
|
-
keys_from_database.each_with_index do |key, key_number|
|
130
|
-
store_raw_data_list_in_cache(workspace_data, index, key, results_from_database[key_number])
|
131
|
-
end
|
132
|
-
|
133
|
-
# combine the results
|
134
|
-
results_from_cache.map do |objs_from_cache|
|
135
|
-
objs_from_cache || results_from_database.shift
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def find_raw_data_from_cache_by(workspace_data, index, key)
|
140
|
-
ContentStateCaching.find_obj_data(workspace_data, index, key) if caching?
|
141
|
-
end
|
142
|
-
|
143
|
-
def find_raw_data_from_database_by(workspace_data, index, keys)
|
144
|
-
return [] if keys.blank?
|
145
|
-
instrumenter = ActiveSupport::Notifications.instrumenter
|
146
|
-
instrumenter.instrument(
|
147
|
-
"cms_load.rails_connector", :name => "Obj Load", :index => index, :keys => keys
|
148
|
-
) do
|
149
|
-
@query_counter += 1
|
150
|
-
queries = ContentServiceObjQueries.new(keys.map {|key| {:type => index, :param => key} })
|
151
|
-
until queries.finished?
|
152
|
-
queries.handle_response(ContentService.query(
|
153
|
-
"objs/query",
|
154
|
-
:queries => queries.open_queries,
|
155
|
-
:workspace_id => workspace_data.id,
|
156
|
-
:revision_id => workspace_data.revision_id
|
157
|
-
))
|
158
|
-
end
|
159
|
-
queries.results
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
UNIQUE_INDICES = [:id, :path, :permalink].freeze
|
164
|
-
|
165
|
-
def store_raw_data_list_in_cache(workspace_data, index, key, raw_data_list)
|
166
|
-
raw_data_list.each do |values|
|
167
|
-
UNIQUE_INDICES.each do |unique_index|
|
168
|
-
unique_index_values = values["_#{unique_index}"]
|
169
|
-
if unique_index_values.present?
|
170
|
-
store_item_in_cache(workspace_data, unique_index, unique_index_values.first, [values])
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
unless UNIQUE_INDICES.include?(index)
|
175
|
-
store_item_in_cache(workspace_data, index, key, raw_data_list)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def store_item_in_cache(workspace_data, index, key, item)
|
180
|
-
ContentStateCaching.store_obj_data(workspace_data, index, key, item)
|
181
|
-
end
|
182
|
-
|
183
|
-
def find_blob_data_from_database_by(id)
|
184
|
-
@query_counter += 1
|
185
|
-
ContentService.query('blobs/query', :blob_ids => [id])['blobs'][id]
|
186
|
-
end
|
187
|
-
|
188
|
-
def assert_valid_index_name(index)
|
189
|
-
raise ArgumentError, "invalid index name '#{index}'" unless VALID_INDEX_NAMES.include?(index)
|
190
|
-
end
|
191
|
-
|
192
|
-
end
|
193
|
-
|
194
|
-
end
|