moab-versioning 4.2.1 → 4.2.2
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/lib/moab.rb +1 -1
- data/lib/moab/bagger.rb +15 -15
- data/lib/moab/config.rb +0 -2
- data/lib/moab/deposit_bag_validator.rb +1 -1
- data/lib/moab/file_group.rb +13 -15
- data/lib/moab/file_group_difference.rb +18 -21
- data/lib/moab/file_group_difference_subset.rb +2 -4
- data/lib/moab/file_instance.rb +1 -3
- data/lib/moab/file_instance_difference.rb +3 -5
- data/lib/moab/file_inventory.rb +17 -27
- data/lib/moab/file_inventory_difference.rb +5 -7
- data/lib/moab/file_manifestation.rb +4 -6
- data/lib/moab/file_signature.rb +29 -40
- data/lib/moab/signature_catalog.rb +11 -13
- data/lib/moab/signature_catalog_entry.rb +1 -3
- data/lib/moab/storage_object.rb +12 -19
- data/lib/moab/storage_object_validator.rb +22 -8
- data/lib/moab/storage_object_version.rb +25 -27
- data/lib/moab/storage_repository.rb +6 -13
- data/lib/moab/storage_services.rb +6 -8
- data/lib/moab/utc_time.rb +0 -2
- data/lib/moab/verification_result.rb +0 -2
- data/lib/moab/version_metadata.rb +1 -3
- data/lib/moab/version_metadata_entry.rb +2 -4
- data/lib/moab/version_metadata_event.rb +0 -2
- data/lib/serializer/manifest.rb +5 -5
- data/lib/serializer/serializable.rb +34 -34
- data/lib/stanford/active_fedora_object.rb +0 -2
- data/lib/stanford/content_inventory.rb +22 -20
- data/lib/stanford/dor_metadata.rb +0 -2
- data/lib/stanford/storage_object_validator.rb +0 -2
- data/lib/stanford/storage_repository.rb +1 -2
- data/lib/stanford/storage_services.rb +5 -7
- metadata +3 -3
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'moab'
|
2
|
-
|
3
1
|
module Moab
|
4
2
|
# A class to represent a version subdirectory within an object's home directory in preservation storage
|
5
3
|
# ====Data Model
|
@@ -32,7 +30,7 @@ module Moab
|
|
32
30
|
def initialize(storage_object, version_id)
|
33
31
|
if version_id.is_a?(Integer)
|
34
32
|
@version_id = version_id
|
35
|
-
elsif version_id.is_a?(String)
|
33
|
+
elsif version_id.is_a?(String) && version_id =~ /^v(\d+)$/
|
36
34
|
@version_id = version_id.sub(/^v/, '').to_i
|
37
35
|
else
|
38
36
|
raise "version_id (#{version_id}) is not in a recognized format"
|
@@ -40,7 +38,7 @@ module Moab
|
|
40
38
|
@version_name = StorageObject.version_dirname(@version_id)
|
41
39
|
@version_pathname = storage_object.object_pathname.join(@version_name)
|
42
40
|
@storage_object = storage_object
|
43
|
-
@inventory_cache =
|
41
|
+
@inventory_cache = {}
|
44
42
|
end
|
45
43
|
|
46
44
|
# @return [String] The unique identifier concatenating digital object id with version id
|
@@ -110,10 +108,10 @@ module Moab
|
|
110
108
|
# @see FileInventory#read_xml_file
|
111
109
|
def file_inventory(type)
|
112
110
|
if version_id > 0
|
113
|
-
return @inventory_cache[type] if @inventory_cache.
|
111
|
+
return @inventory_cache[type] if @inventory_cache.key?(type)
|
114
112
|
@inventory_cache[type] = FileInventory.read_xml_file(@version_pathname.join('manifests'), type)
|
115
113
|
else
|
116
|
-
groups = [
|
114
|
+
groups = %w[content metadata].collect { |id| FileGroup.new(:group_id => id) }
|
117
115
|
FileInventory.new(
|
118
116
|
:type => 'version',
|
119
117
|
:digital_object_id => @storage_object.digital_object_id,
|
@@ -208,12 +206,12 @@ module Moab
|
|
208
206
|
end
|
209
207
|
|
210
208
|
# @return [VerificationResult] return result of testing correctness of version manifests
|
211
|
-
def verify_version_storage
|
212
|
-
result = VerificationResult.new(
|
213
|
-
result.subentities <<
|
214
|
-
result.subentities <<
|
215
|
-
result.subentities <<
|
216
|
-
result.verified = result.subentities.all?
|
209
|
+
def verify_version_storage
|
210
|
+
result = VerificationResult.new(composite_key)
|
211
|
+
result.subentities << verify_manifest_inventory
|
212
|
+
result.subentities << verify_version_inventory
|
213
|
+
result.subentities << verify_version_additions
|
214
|
+
result.verified = result.subentities.all?(&:verified)
|
217
215
|
result
|
218
216
|
end
|
219
217
|
|
@@ -221,8 +219,8 @@ module Moab
|
|
221
219
|
def verify_manifest_inventory
|
222
220
|
# read/parse manifestInventory.xml
|
223
221
|
result = VerificationResult.new("manifest_inventory")
|
224
|
-
manifest_inventory =
|
225
|
-
result.subentities << VerificationResult.verify_value('composite_key',
|
222
|
+
manifest_inventory = file_inventory('manifests')
|
223
|
+
result.subentities << VerificationResult.verify_value('composite_key', composite_key, manifest_inventory.composite_key)
|
226
224
|
result.subentities << VerificationResult.verify_truth('manifests_group', !manifest_inventory.group_empty?('manifests'))
|
227
225
|
# measure the manifest signatures of the files in the directory (excluding manifestInventory.xml)
|
228
226
|
directory_inventory = FileInventory.new.inventory_from_directory(@version_pathname.join('manifests'), 'manifests')
|
@@ -236,7 +234,7 @@ module Moab
|
|
236
234
|
compare_result.verified = (diff.difference_count == 0)
|
237
235
|
compare_result.details = diff.differences_detail
|
238
236
|
result.subentities << compare_result
|
239
|
-
result.verified = result.subentities.all?
|
237
|
+
result.verified = result.subentities.all?(&:verified)
|
240
238
|
result
|
241
239
|
end
|
242
240
|
|
@@ -244,10 +242,10 @@ module Moab
|
|
244
242
|
def verify_signature_catalog
|
245
243
|
result = VerificationResult.new("signature_catalog")
|
246
244
|
signature_catalog = self.signature_catalog
|
247
|
-
result.subentities << VerificationResult.verify_value('signature_key',
|
245
|
+
result.subentities << VerificationResult.verify_value('signature_key', composite_key, signature_catalog.composite_key)
|
248
246
|
found = 0
|
249
|
-
missing =
|
250
|
-
object_pathname =
|
247
|
+
missing = []
|
248
|
+
object_pathname = storage_object.object_pathname
|
251
249
|
signature_catalog.entries.each do |catalog_entry|
|
252
250
|
storage_location = object_pathname.join(catalog_entry.storage_path)
|
253
251
|
if storage_location.exist?
|
@@ -264,19 +262,19 @@ module Moab
|
|
264
262
|
}
|
265
263
|
file_result.details['missing'] = missing unless missing.empty?
|
266
264
|
result.subentities << file_result
|
267
|
-
result.verified = result.subentities.all?
|
265
|
+
result.verified = result.subentities.all?(&:verified)
|
268
266
|
result
|
269
267
|
end
|
270
268
|
|
271
269
|
# @return [Boolean] true if files & signatures listed in version inventory can all be found
|
272
270
|
def verify_version_inventory
|
273
271
|
result = VerificationResult.new("version_inventory")
|
274
|
-
version_inventory =
|
275
|
-
result.subentities << VerificationResult.verify_value('inventory_key',
|
272
|
+
version_inventory = file_inventory('version')
|
273
|
+
result.subentities << VerificationResult.verify_value('inventory_key', composite_key, version_inventory.composite_key)
|
276
274
|
signature_catalog = self.signature_catalog
|
277
|
-
result.subentities << VerificationResult.verify_value('signature_key',
|
275
|
+
result.subentities << VerificationResult.verify_value('signature_key', composite_key, signature_catalog.composite_key)
|
278
276
|
found = 0
|
279
|
-
missing =
|
277
|
+
missing = []
|
280
278
|
version_inventory.groups.each do |group|
|
281
279
|
group.files.each do |file|
|
282
280
|
file.instances.each do |instance|
|
@@ -298,15 +296,15 @@ module Moab
|
|
298
296
|
}
|
299
297
|
file_result.details['missing'] = missing unless missing.empty?
|
300
298
|
result.subentities << file_result
|
301
|
-
result.verified = result.subentities.all?
|
299
|
+
result.verified = result.subentities.all?(&:verified)
|
302
300
|
result
|
303
301
|
end
|
304
302
|
|
305
303
|
# @return [Boolean] returns true if files in data folder match files listed in version addtions inventory
|
306
304
|
def verify_version_additions
|
307
305
|
result = VerificationResult.new("version_additions")
|
308
|
-
version_additions =
|
309
|
-
result.subentities << VerificationResult.verify_value('composite_key',
|
306
|
+
version_additions = file_inventory('additions')
|
307
|
+
result.subentities << VerificationResult.verify_value('composite_key', composite_key, version_additions.composite_key)
|
310
308
|
data_directory = @version_pathname.join('data')
|
311
309
|
directory_inventory = FileInventory.new(:type => 'directory').inventory_from_directory(data_directory)
|
312
310
|
diff = FileInventoryDifference.new
|
@@ -315,7 +313,7 @@ module Moab
|
|
315
313
|
compare_result.verified = (diff.difference_count == 0)
|
316
314
|
compare_result.details = diff.differences_detail
|
317
315
|
result.subentities << compare_result
|
318
|
-
result.verified = result.subentities.all?
|
316
|
+
result.verified = result.subentities.all?(&:verified)
|
319
317
|
result
|
320
318
|
end
|
321
319
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'moab'
|
2
|
-
|
3
1
|
module Moab
|
4
2
|
# A class to represent the SDR repository store
|
5
3
|
#
|
@@ -58,9 +56,9 @@ module Moab
|
|
58
56
|
end
|
59
57
|
|
60
58
|
# @param object_id [String] The identifier of the digital object
|
61
|
-
# @return [
|
59
|
+
# @return [String] The branch segment of the object deposit path
|
60
|
+
# @note Override this method in a subclass
|
62
61
|
def deposit_branch(object_id)
|
63
|
-
#todo This method should be customized, or overridden in a subclass
|
64
62
|
object_id
|
65
63
|
end
|
66
64
|
|
@@ -77,7 +75,7 @@ module Moab
|
|
77
75
|
return root if root_trunk_branch.exist?
|
78
76
|
end
|
79
77
|
# Search for the object's directory in the deposit areas
|
80
|
-
if include_deposit
|
78
|
+
if include_deposit && deposit_trunk
|
81
79
|
branch = deposit_branch(object_id)
|
82
80
|
storage_roots.each do |root|
|
83
81
|
root_trunk = root.join(deposit_trunk)
|
@@ -119,11 +117,8 @@ module Moab
|
|
119
117
|
def storage_object(object_id, create = false)
|
120
118
|
storage_object = find_storage_object(object_id)
|
121
119
|
unless storage_object.object_pathname.exist?
|
122
|
-
|
123
|
-
|
124
|
-
else
|
125
|
-
raise Moab::ObjectNotFoundException, "No storage object found for #{object_id}"
|
126
|
-
end
|
120
|
+
raise Moab::ObjectNotFoundException, "No storage object found for #{object_id}" unless create
|
121
|
+
storage_object.object_pathname.mkpath
|
127
122
|
end
|
128
123
|
storage_object
|
129
124
|
end
|
@@ -132,9 +127,7 @@ module Moab
|
|
132
127
|
# @param druid [String] The object identifier
|
133
128
|
# @return [void] transfer the object to the preservation repository
|
134
129
|
def store_new_object_version(druid, bag_pathname)
|
135
|
-
storage_object
|
136
|
-
new_version = storage_object.ingest_bag(bag_pathname)
|
137
|
-
new_version
|
130
|
+
storage_object(druid, create = true).ingest_bag(bag_pathname)
|
138
131
|
end
|
139
132
|
end
|
140
133
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'moab'
|
2
|
-
|
3
1
|
module Moab
|
4
2
|
# An interface class to support access to SDR storage via a RESTful server
|
5
3
|
#
|
@@ -79,7 +77,7 @@ module Moab
|
|
79
77
|
# @param [String] object_id The digital object identifier of the object
|
80
78
|
# @return [Pathname] Pathname object containing the full path for the specified file
|
81
79
|
def self.version_metadata(object_id)
|
82
|
-
|
80
|
+
retrieve_file('metadata', 'versionMetadata.xml', object_id)
|
83
81
|
end
|
84
82
|
|
85
83
|
# @param [String] object_id The digital object identifier of the object
|
@@ -87,11 +85,11 @@ module Moab
|
|
87
85
|
# @return [FileInventory] the file inventory for the specified object version
|
88
86
|
def self.retrieve_file_group(file_category, object_id, version_id = nil)
|
89
87
|
storage_object_version = @@repository.storage_object(object_id).find_object_version(version_id)
|
90
|
-
if file_category =~ /manifest/
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
88
|
+
inventory_type = if file_category =~ /manifest/
|
89
|
+
file_category = 'manifests'
|
90
|
+
else
|
91
|
+
'version'
|
92
|
+
end
|
95
93
|
inventory = storage_object_version.file_inventory(inventory_type)
|
96
94
|
inventory.group(file_category)
|
97
95
|
end
|
data/lib/moab/utc_time.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'moab'
|
2
|
-
|
3
1
|
module Moab
|
4
2
|
# The descriptive information about a digital object's collection of versions
|
5
3
|
#
|
@@ -19,7 +17,7 @@ module Moab
|
|
19
17
|
|
20
18
|
# (see Serializable#initialize)
|
21
19
|
def initialize(opts = {})
|
22
|
-
@versions =
|
20
|
+
@versions = []
|
23
21
|
super(opts)
|
24
22
|
end
|
25
23
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'moab'
|
2
|
-
|
3
1
|
module Moab
|
4
2
|
# The descriptive attributes of a digital object version.
|
5
3
|
#
|
@@ -18,13 +16,13 @@ module Moab
|
|
18
16
|
|
19
17
|
# (see Serializable#initialize)
|
20
18
|
def initialize(opts = {})
|
21
|
-
@events =
|
19
|
+
@events = []
|
22
20
|
super(opts)
|
23
21
|
end
|
24
22
|
|
25
23
|
# @attribute
|
26
24
|
# @return [Integer] The object version number (A sequential integer)
|
27
|
-
attribute :version_id, Integer, :tag => 'versionId', :key => true, :on_save =>
|
25
|
+
attribute :version_id, Integer, :tag => 'versionId', :key => true, :on_save => proc { |n| n.to_s }
|
28
26
|
|
29
27
|
# @attribute
|
30
28
|
# @return [String] "an external version label that increments the most significant digit for major revisions,
|
data/lib/serializer/manifest.rb
CHANGED
@@ -19,7 +19,7 @@ module Serializer
|
|
19
19
|
if filename
|
20
20
|
filename
|
21
21
|
else
|
22
|
-
cname =
|
22
|
+
cname = name.split(/::/).last
|
23
23
|
cname[0, 1].downcase + cname[1..-1] + '.xml'
|
24
24
|
end
|
25
25
|
end
|
@@ -29,7 +29,7 @@ module Serializer
|
|
29
29
|
# @param filename [String] Optional filename if one wishes to override the default filename
|
30
30
|
# @return [Pathname] The location of the xml file
|
31
31
|
def self.xml_pathname(parent_dir, filename = nil)
|
32
|
-
Pathname.new(parent_dir).join(
|
32
|
+
Pathname.new(parent_dir).join(xml_filename(filename))
|
33
33
|
end
|
34
34
|
|
35
35
|
# @api external
|
@@ -37,7 +37,7 @@ module Serializer
|
|
37
37
|
# @param filename [String] Optional filename if one wishes to override the default filename
|
38
38
|
# @return [Boolean] Returns true if the xml file exists
|
39
39
|
def self.xml_pathname_exist?(parent_dir, filename = nil)
|
40
|
-
|
40
|
+
xml_pathname(parent_dir, filename).exist?
|
41
41
|
end
|
42
42
|
|
43
43
|
# @api external
|
@@ -46,7 +46,7 @@ module Serializer
|
|
46
46
|
# @return [Serializable] Read the xml file and return the parsed XML
|
47
47
|
# @example {include:file:spec/features/serializer/read_xml_spec.rb}
|
48
48
|
def self.read_xml_file(parent_dir, filename = nil)
|
49
|
-
|
49
|
+
parse(xml_pathname(parent_dir, filename).read)
|
50
50
|
end
|
51
51
|
|
52
52
|
# @api external
|
@@ -56,7 +56,7 @@ module Serializer
|
|
56
56
|
# @return [void] Serializize the in-memory object to a xml file instance
|
57
57
|
def self.write_xml_file(xml_object, parent_dir, filename = nil)
|
58
58
|
parent_dir.mkpath
|
59
|
-
|
59
|
+
xml_pathname(parent_dir, filename).open('w') do |f|
|
60
60
|
xmlBuilder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8')
|
61
61
|
xmlBuilder = xml_object.to_xml(xmlBuilder)
|
62
62
|
f << xmlBuilder.to_xml
|
@@ -19,11 +19,8 @@ module Serializer
|
|
19
19
|
# The symbols should correspond to attributes declared using HappyMapper syntax
|
20
20
|
def initialize(opts = {})
|
21
21
|
opts.each do |key, value|
|
22
|
-
|
23
|
-
|
24
|
-
else
|
25
|
-
raise "#{key} is not a variable name in #{self.class.name}"
|
26
|
-
end
|
22
|
+
raise "#{key} is not a variable name in #{self.class.name}" unless variable_names.include?(key.to_s) || key == :test
|
23
|
+
instance_variable_set("@#{key}", value)
|
27
24
|
end
|
28
25
|
end
|
29
26
|
|
@@ -45,7 +42,7 @@ module Serializer
|
|
45
42
|
# @api internal
|
46
43
|
# @return [Array] Extract the names of the variables
|
47
44
|
def variable_names
|
48
|
-
variables.collect
|
45
|
+
variables.collect(&:name)
|
49
46
|
end
|
50
47
|
|
51
48
|
# @api internal
|
@@ -54,7 +51,7 @@ module Serializer
|
|
54
51
|
# This follows the same convention as used by DataMapper
|
55
52
|
# @see http://datamapper.org/docs/properties.html
|
56
53
|
def key_name
|
57
|
-
|
54
|
+
unless defined?(@key_name)
|
58
55
|
@key_name = nil
|
59
56
|
self.class.attributes.each do |attribute|
|
60
57
|
if attribute.options[:key]
|
@@ -69,7 +66,7 @@ module Serializer
|
|
69
66
|
# @api internal
|
70
67
|
# @return [String] For the current object instance, return the string to use as a hash key
|
71
68
|
def key
|
72
|
-
return
|
69
|
+
return send(key_name) if key_name
|
73
70
|
nil
|
74
71
|
end
|
75
72
|
|
@@ -79,10 +76,10 @@ module Serializer
|
|
79
76
|
# If the array member has a field tagged as a key, that field will be used as the hash.key.
|
80
77
|
# Otherwise the index position of the array member will be used as the key
|
81
78
|
def array_to_hash(array, summary = false)
|
82
|
-
item_hash =
|
79
|
+
item_hash = {}
|
83
80
|
array.each_index do |index|
|
84
81
|
item = array[index]
|
85
|
-
ikey =
|
82
|
+
ikey = item.respond_to?(:key) && item.key ? item.key : index
|
86
83
|
item_hash[ikey] = item.respond_to?(:to_hash) ? item.to_hash(summary) : item
|
87
84
|
end
|
88
85
|
item_hash
|
@@ -92,26 +89,26 @@ module Serializer
|
|
92
89
|
# @return [Hash] Recursively generate an Hash containing the object's properties
|
93
90
|
# @param summary [Boolean] Controls the depth and detail of recursion
|
94
91
|
def to_hash(summary = false)
|
95
|
-
oh =
|
92
|
+
oh = {}
|
96
93
|
vars = summary ? variables.select { |v| summary_fields.include?(v.name) } : variables
|
97
94
|
vars.each do |variable|
|
98
95
|
key = variable.name.to_s
|
99
|
-
value =
|
100
|
-
case value
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
96
|
+
value = send(variable.name)
|
97
|
+
oh[key] = case value
|
98
|
+
when Array
|
99
|
+
array_to_hash(value, summary)
|
100
|
+
when Serializable
|
101
|
+
value.to_hash
|
102
|
+
else
|
103
|
+
value
|
104
|
+
end
|
108
105
|
end
|
109
106
|
oh
|
110
107
|
end
|
111
108
|
|
112
109
|
# @return [Hash] Calls to_hash(summary=true)
|
113
110
|
def summary
|
114
|
-
|
111
|
+
to_hash(summary = true)
|
115
112
|
end
|
116
113
|
|
117
114
|
# @api internal
|
@@ -120,13 +117,13 @@ module Serializer
|
|
120
117
|
def diff(other)
|
121
118
|
raise "Cannot compare different classes" if self.class != other.class
|
122
119
|
left = other.to_hash
|
123
|
-
right =
|
124
|
-
if
|
120
|
+
right = to_hash
|
121
|
+
if key.nil? || other.key.nil?
|
125
122
|
ltag = :old
|
126
123
|
rtag = :new
|
127
124
|
else
|
128
125
|
ltag = other.key
|
129
|
-
rtag =
|
126
|
+
rtag = key
|
130
127
|
end
|
131
128
|
Serializable.deep_diff(ltag, left, rtag, right)
|
132
129
|
end
|
@@ -136,23 +133,26 @@ module Serializer
|
|
136
133
|
# @return [Hash] Generate a hash containing the differences between two hashes
|
137
134
|
# (recursively descend parallel trees of hashes)
|
138
135
|
# @see https://gist.github.com/146844
|
139
|
-
def
|
140
|
-
diff =
|
136
|
+
def self.deep_diff(*hashes)
|
137
|
+
diff = {}
|
141
138
|
case hashes.length
|
142
139
|
when 4
|
143
140
|
ltag, left, rtag, right = hashes
|
144
141
|
when 2
|
145
|
-
ltag
|
142
|
+
ltag = :left
|
143
|
+
left = hashes[0]
|
144
|
+
rtag = :right
|
145
|
+
right = hashes[1]
|
146
146
|
else
|
147
147
|
raise ArgumentError, "wrong number of arguments (#{hashes.length} for 2 or 4)"
|
148
148
|
end
|
149
149
|
(left.keys | right.keys).each do |k|
|
150
150
|
if left[k] != right[k]
|
151
|
-
if left[k].is_a?(Hash) && right[k].is_a?(Hash)
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
151
|
+
diff[k] = if left[k].is_a?(Hash) && right[k].is_a?(Hash)
|
152
|
+
deep_diff(ltag, left[k], rtag, right[k])
|
153
|
+
else
|
154
|
+
Hash.[](ltag, left[k], rtag, right[k])
|
155
|
+
end
|
156
156
|
end
|
157
157
|
end
|
158
158
|
diff
|
@@ -161,14 +161,14 @@ module Serializer
|
|
161
161
|
# @api internal
|
162
162
|
# @return [String] Generate JSON output from a hash of the object's variables
|
163
163
|
def to_json(summary = false)
|
164
|
-
hash =
|
164
|
+
hash = to_hash(summary)
|
165
165
|
JSON.pretty_generate(hash)
|
166
166
|
end
|
167
167
|
|
168
168
|
# @api internal
|
169
169
|
# @return [String] Generate YAML output from a hash of the object's variables
|
170
170
|
def to_yaml(summary = false)
|
171
|
-
|
171
|
+
to_hash(summary).to_yaml
|
172
172
|
end
|
173
173
|
end
|
174
174
|
end
|