hydra-derivatives 3.5.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +56 -23
  3. data/Gemfile +3 -2
  4. data/README.md +16 -5
  5. data/Rakefile +1 -1
  6. data/VERSION +1 -1
  7. data/hydra-derivatives.gemspec +9 -6
  8. data/lib/hydra/derivatives/config.rb +1 -1
  9. data/lib/hydra/derivatives/processors/document.rb +4 -1
  10. data/lib/hydra/derivatives/processors/full_text.rb +5 -3
  11. data/lib/hydra/derivatives/processors/image.rb +4 -2
  12. data/solr/config/xslt/example.xsl +1 -1
  13. data/solr/config/xslt/luke.xsl +1 -1
  14. metadata +134 -94
  15. data/spec/fixtures/FlashPix.ppt +0 -0
  16. data/spec/fixtures/adobe1998.tif +0 -0
  17. data/spec/fixtures/countdown.avi +0 -0
  18. data/spec/fixtures/jpeg2k_config.yml +0 -20
  19. data/spec/fixtures/piano_note.wav +0 -0
  20. data/spec/fixtures/sample.rtf +0 -68
  21. data/spec/fixtures/test.dng +0 -0
  22. data/spec/fixtures/test.doc +0 -0
  23. data/spec/fixtures/test.docx +0 -0
  24. data/spec/fixtures/test.pdf +0 -0
  25. data/spec/fixtures/test.pptx +0 -0
  26. data/spec/fixtures/test.tif +0 -0
  27. data/spec/fixtures/test.xls +0 -0
  28. data/spec/fixtures/test.xlsx +0 -0
  29. data/spec/fixtures/world.png +0 -0
  30. data/spec/processors/active_encode_spec.rb +0 -132
  31. data/spec/processors/document_spec.rb +0 -41
  32. data/spec/processors/full_text_spec.rb +0 -127
  33. data/spec/processors/image_spec.rb +0 -124
  34. data/spec/processors/jpeg2k_spec.rb +0 -82
  35. data/spec/processors/processor_spec.rb +0 -53
  36. data/spec/processors/shell_based_processor_spec.rb +0 -28
  37. data/spec/processors/video_spec.rb +0 -59
  38. data/spec/runners/active_encode_derivatives_spec.rb +0 -38
  39. data/spec/runners/runner_spec.rb +0 -9
  40. data/spec/services/audio_derivatives_spec.rb +0 -78
  41. data/spec/services/persist_basic_contained_output_file_service_spec.rb +0 -42
  42. data/spec/services/persist_external_file_output_file_service_spec.rb +0 -26
  43. data/spec/services/persist_output_file_service_spec.rb +0 -47
  44. data/spec/services/remote_source_file_spec.rb +0 -33
  45. data/spec/services/retrieve_source_file_service_spec.rb +0 -60
  46. data/spec/services/tempfile_service_spec.rb +0 -54
  47. data/spec/spec_helper.rb +0 -40
  48. data/spec/units/audio_encoder_spec.rb +0 -34
  49. data/spec/units/config_spec.rb +0 -50
  50. data/spec/units/derivatives_spec.rb +0 -70
  51. data/spec/units/io_decorator_spec.rb +0 -45
  52. data/spec/units/logger_spec.rb +0 -27
  53. data/spec/units/transcoding_spec.rb +0 -354
@@ -1,70 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Hydra::Derivatives do
4
- before(:all) do
5
- class CustomFile < ActiveFedora::Base
6
- include Hydra::Derivatives
7
- end
8
- end
9
-
10
- after(:all) { Object.send(:remove_const, :CustomFile) }
11
-
12
- describe "source_file_service" do
13
- before { subject.source_file_service = custom_source_file_service }
14
-
15
- context "with a global configuration setting" do
16
- subject { CustomFile }
17
-
18
- let(:custom_source_file_service) { "fake service" }
19
-
20
- it "utilizes the default source file service" do
21
- expect(subject.source_file_service).to eq(custom_source_file_service)
22
- end
23
- end
24
-
25
- context "with an instance level configuration setting" do
26
- subject { CustomFile.new }
27
-
28
- let(:custom_source_file_service) { "another fake service" }
29
-
30
- it "accepts a custom source file service as an option" do
31
- expect(subject.source_file_service).to eq(custom_source_file_service)
32
- end
33
- end
34
- end
35
-
36
- Hydra::Derivatives::CONFIG_METHODS.each do |method|
37
- describe method.to_s do
38
- it 'returns the config value' do
39
- expect(subject.send(method)).to eq subject.config.send(method)
40
- end
41
- end
42
- describe "#{method}=" do
43
- around do |example|
44
- value = subject.config.send(method)
45
- example.call
46
- subject.send("#{method}=", value)
47
- end
48
-
49
- it 'stores config changes' do
50
- expect { subject.send("#{method}=", "new_value") }.to change { subject.config.send(method) }.from(subject.config.send(method)).to("new_value")
51
- end
52
- end
53
- end
54
-
55
- describe 'reset_config!' do
56
- it "resets the configuration" do
57
- subject.ffmpeg_path = '/usr/local/ffmpeg-1.0/bin/ffmpeg'
58
- subject.reset_config!
59
- expect(subject.ffmpeg_path).to eq('ffmpeg')
60
-
61
- subject.kdu_compress_path = '/usr/local/bin/kdu_compress'
62
- subject.reset_config!
63
- expect(subject.kdu_compress_path).to eq('kdu_compress')
64
-
65
- subject.active_encode_poll_time = 2
66
- subject.reset_config!
67
- expect(subject.active_encode_poll_time).to eq 10
68
- end
69
- end
70
- end
@@ -1,45 +0,0 @@
1
- require 'spec_helper'
2
- require 'stringio'
3
-
4
- describe Hydra::Derivatives::IoDecorator do
5
- let(:file) { StringIO.new('hello') }
6
-
7
- context "with one argument" do
8
- let(:decorator) { described_class.new(file) }
9
-
10
- describe "#read" do
11
- subject { decorator.read }
12
-
13
- it { is_expected.to eq 'hello' }
14
- end
15
- end
16
-
17
- context "with three arguments" do
18
- let(:decorator) { described_class.new(file, 'text/plain', 'help.txt') }
19
-
20
- describe "#read" do
21
- subject { decorator.read }
22
-
23
- it { is_expected.to eq 'hello' }
24
- end
25
-
26
- describe "mime_type" do
27
- subject { decorator.mime_type }
28
-
29
- it { is_expected.to eq 'text/plain' }
30
- end
31
-
32
- describe "original_filename" do
33
- subject { decorator.original_filename }
34
-
35
- it { is_expected.to eq 'help.txt' }
36
- end
37
-
38
- describe "original_name" do
39
- subject { decorator.original_name }
40
-
41
- before { allow(Deprecation).to receive(:warn) }
42
- it { is_expected.to eq 'help.txt' }
43
- end
44
- end
45
- end
@@ -1,27 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Hydra::Derivatives::Logger do
4
- context "with log levels" do
5
- let(:levels) { %w[unknown fatal error warn info debug] }
6
-
7
- it "responds successfully" do
8
- levels.each do |level|
9
- expect(described_class.respond_to?(level)).to be_truthy
10
- end
11
- end
12
- it "accepts messages" do
13
- expect(described_class.warn("message")).to be_truthy
14
- end
15
- end
16
-
17
- it "delegates respond_to_missing" do
18
- allow(ActiveFedora::Base.logger).to receive(:respond_to_missing?).with(:weird, false).and_return(true)
19
- expect(described_class.respond_to_missing?(:weird)).to be_truthy
20
- end
21
-
22
- context "with garbage" do
23
- it "raises an error" do
24
- expect { described_class.garbage }.to raise_error(NoMethodError)
25
- end
26
- end
27
- end
@@ -1,354 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe "Transcoding" do
4
- before(:all) do
5
- class GenericFile < ActiveFedora::Base
6
- include Hydra::Derivatives
7
- property :mime_type_from_fits, predicate: ::RDF::URI('http://example.com/mime'), multiple: false
8
- has_subresource 'original_file'
9
-
10
- def create_derivatives(_filename)
11
- case mime_type_from_fits
12
- when 'application/pdf'
13
- PdfDerivatives.create(self, source: :original_file,
14
- outputs: [{ label: :thumb, size: "100x100>", url: "#{uri}/original_file_thumb" }])
15
- FullTextExtract.create(self, source: :original_file, outputs: [{ url: "#{uri}/fulltext" }])
16
- when 'audio/x-wav'
17
- AudioDerivatives.create(self, source: :original_file,
18
- outputs: [
19
- { label: :mp3, format: 'mp3', url: "#{uri}/original_file_mp3" },
20
- { label: :ogg, format: 'ogg', url: "#{uri}/original_file_ogg" }
21
- ])
22
- when 'video/x-msvideo'
23
- VideoDerivatives.create(self, source: :original_file,
24
- outputs: [
25
- { label: :mp4, format: 'mp4', url: "#{uri}/original_file_mp4" },
26
- { label: :webm, format: 'webm', url: "#{uri}/original_file_webm" },
27
- { label: :thumbnail, format: 'jpg', url: "#{uri}/thumbnail" }
28
- ])
29
- when 'image/png', 'image/jpg'
30
- ImageDerivatives.create(self, source: :original_file,
31
- outputs: [
32
- { label: :medium, size: "300x300>", url: "#{uri}/original_file_medium" },
33
- { label: :thumb, size: "100x100>", url: "#{uri}/original_file_thumb" },
34
- { label: :access, format: 'jpg', url: "#{uri}/access" }
35
- ])
36
- when 'application/vnd.ms-powerpoint'
37
- DocumentDerivatives.create(self, source: :original_file,
38
- outputs: [
39
- { label: :preservation, format: 'pptx', url: "#{uri}/original_file_preservation" },
40
- { label: :access, format: 'pdf', url: "#{uri}/original_file_access" },
41
- { label: :thumnail, format: 'jpg', url: "#{uri}/original_file_thumbnail" }
42
- ])
43
- when 'text/rtf'
44
- DocumentDerivatives.create(self, source: :original_file,
45
- outputs: [
46
- { label: :preservation, format: 'odt', url: "#{uri}/original_file_preservation" },
47
- { label: :access, format: 'pdf', url: "#{uri}/original_file_access" },
48
- { label: :thumnail, format: 'jpg', url: "#{uri}/original_file_thumbnail" }
49
- ])
50
- when 'application/msword'
51
- DocumentDerivatives.create(self, source: :original_file,
52
- outputs: [
53
- { label: :preservation, format: 'docx', url: "#{uri}/original_file_preservation" },
54
- { label: :access, format: 'pdf', url: "#{uri}/original_file_access" },
55
- { label: :thumnail, format: 'jpg', url: "#{uri}/original_file_thumbnail" }
56
- ])
57
- when 'application/vnd.ms-excel'
58
- DocumentDerivatives.create(self, source: :original_file,
59
- outputs: [
60
- { label: :preservation, format: 'xlsx', url: "#{uri}/original_file_preservation" },
61
- { label: :access, format: 'pdf', url: "#{uri}/original_file_access" },
62
- { label: :thumnail, format: 'jpg', url: "#{uri}/original_file_thumbnail" }
63
- ])
64
- when 'image/tiff'
65
- Jpeg2kImageDerivatives.create(self, source: :original_file,
66
- outputs: [
67
- { label: :resized, format: 'jp2', recipe: :default, processor: 'jpeg2k_image', resize: "600x600>", url: "#{uri}/resized" },
68
- { label: :config_lookup, format: 'jp2', recipe: :default, processor: 'jpeg2k_image', url: "#{uri}/config_lookup" },
69
- { label: :string_recipe, format: 'jp2', recipe: '-jp2_space sRGB', processor: 'jpeg2k_image', url: "#{uri}/string_recipe" },
70
- { label: :diy, format: 'jp2', processor: 'jpeg2k_image', url: "#{uri}/original_file_diy" }
71
- ])
72
- when 'image/x-adobe-dng'
73
- ImageDerivatives.create(self, source: :original_file,
74
- outputs: [
75
- { label: :access, size: "300x300>", format: 'jpg', processor: :raw_image, url: "#{uri}/original_file_access" },
76
- { label: :thumb, size: "100x100>", format: 'jpg', processor: :raw_image, url: "#{uri}/original_file_thumb" }
77
- ])
78
- end
79
- end
80
- end
81
- end
82
-
83
- after(:all) do
84
- Object.send(:remove_const, :GenericFile)
85
- end
86
-
87
- describe "with an attached image" do
88
- let(:filename) { File.expand_path('../../fixtures/world.png', __FILE__) }
89
- let(:attachment) { File.open(filename) }
90
- let(:file) do
91
- GenericFile.new(mime_type_from_fits: 'image/png') do |f|
92
- f.original_file.content = attachment
93
- f.original_file.mime_type = f.mime_type_from_fits
94
- f.save!
95
- end
96
- end
97
-
98
- it "transcodes" do
99
- expect(file.attached_files.key?('original_file_medium')).to be_falsey
100
- file.create_derivatives(filename)
101
- file.reload
102
- expect(file.attached_files['original_file_medium']).to have_content
103
- expect(file.attached_files['original_file_medium'].mime_type).to eq('image/png')
104
- expect(file.attached_files['original_file_thumb']).to have_content
105
- expect(file.attached_files['original_file_thumb'].mime_type).to eq('image/png')
106
- expect(file.attached_files['access']).to have_content
107
- expect(file.attached_files['access'].mime_type).to eq('image/jpeg')
108
- expect(file.attached_files.key?('original_file_text')).to be_falsey
109
- end
110
- end
111
-
112
- describe "with an attached RAW image", requires_imagemagick: true do
113
- let(:filename) { File.expand_path('../../fixtures/test.dng', __FILE__) }
114
- let(:attachment) { File.open(filename) }
115
- let(:file) do
116
- GenericFile.new(mime_type_from_fits: 'image/x-adobe-dng') do |f|
117
- f.original_file.content = attachment
118
- f.original_file.mime_type = f.mime_type_from_fits
119
- f.save!
120
- end
121
- end
122
-
123
- it "transcodes" do
124
- expect(file.attached_files.key?('original_file_access')).to be_falsey
125
- expect(file.attached_files.key?('original_file_thumb')).to be_falsey
126
-
127
- file.create_derivatives(filename)
128
- file.reload
129
- expect(file.attached_files['original_file_access']).to have_content
130
- expect(file.attached_files['original_file_access'].mime_type).to eq('image/jpeg')
131
- expect(file.attached_files['original_file_thumb']).to have_content
132
- expect(file.attached_files['original_file_thumb'].mime_type).to eq('image/jpeg')
133
- end
134
- end
135
-
136
- describe "with an attached pdf", requires_imagemagick: true do
137
- let(:filename) { File.expand_path('../../fixtures/test.pdf', __FILE__) }
138
- let(:attachment) { File.open(filename) }
139
- let(:file) do
140
- GenericFile.new(mime_type_from_fits: 'application/pdf') do |t|
141
- t.original_file.content = attachment
142
- t.original_file.mime_type = t.mime_type_from_fits
143
- t.save
144
- end
145
- end
146
-
147
- it "transcodes" do
148
- expect(file.attached_files.key?('original_file_thumb')).to be_falsey
149
- file.create_derivatives(filename)
150
- file.reload
151
- expect(file.attached_files['original_file_thumb']).to have_content
152
- expect(file.attached_files['original_file_thumb'].mime_type).to eq('image/png')
153
- expect(file.attached_files['fulltext'].content).to match(/This PDF file was created using CutePDF/)
154
- expect(file.attached_files['fulltext'].mime_type).to eq 'text/plain;charset=UTF-8'
155
- end
156
- end
157
-
158
- describe "with an attached audio", requires_ffmpeg: true do
159
- let(:filename) { File.expand_path('../../fixtures/piano_note.wav', __FILE__) }
160
- let(:attachment) { File.open(filename) }
161
- let(:file) do
162
- GenericFile.new(mime_type_from_fits: 'audio/x-wav').tap do |t|
163
- t.original_file.content = attachment
164
- t.original_file.mime_type = t.mime_type_from_fits
165
- t.save
166
- end
167
- end
168
-
169
- it "transcodes" do
170
- file.create_derivatives(filename)
171
- file.reload
172
- expect(file.attached_files['original_file_mp3']).to have_content
173
- expect(file.attached_files['original_file_mp3'].mime_type).to eq('audio/mpeg')
174
- expect(file.attached_files['original_file_ogg']).to have_content
175
- expect(file.attached_files['original_file_ogg'].mime_type).to eq('audio/ogg')
176
- end
177
- end
178
-
179
- describe "when the source datastrem has an unknown mime_type", requires_ffmpeg: true do
180
- let(:filename) { File.expand_path('../../fixtures/piano_note.wav', __FILE__) }
181
- let(:attachment) { File.open(filename) }
182
- let(:file) do
183
- GenericFile.new(mime_type_from_fits: 'audio/x-wav').tap do |t|
184
- t.original_file.content = attachment
185
- t.original_file.mime_type = 'audio/vnd.wav'
186
- t.save
187
- end
188
- end
189
-
190
- it "transcodes" do
191
- allow_any_instance_of(::Logger).to receive(:warn)
192
- file.create_derivatives(filename)
193
- file.reload
194
- expect(file.attached_files['original_file_mp3']).to have_content
195
- expect(file.attached_files['original_file_mp3'].mime_type).to eq('audio/mpeg')
196
- end
197
- end
198
-
199
- describe "with an attached video", requires_ffmpeg: true do
200
- let(:filename) { File.expand_path('../../fixtures/countdown.avi', __FILE__) }
201
- let(:attachment) { File.open(filename) }
202
- let(:file) do
203
- GenericFile.create(mime_type_from_fits: 'video/x-msvideo') do |t|
204
- t.original_file.content = attachment
205
- t.original_file.mime_type = t.mime_type_from_fits
206
- t.save
207
- end
208
- end
209
-
210
- it "transcodes" do
211
- file.create_derivatives(filename)
212
- file.reload
213
- expect(file.attached_files['original_file_mp4']).to have_content
214
- expect(file.attached_files['original_file_mp4'].mime_type).to eq('video/mp4')
215
- expect(file.attached_files['original_file_webm']).to have_content
216
- expect(file.attached_files['original_file_webm'].mime_type).to eq('video/webm')
217
- expect(file.attached_files['thumbnail']).to have_content
218
- expect(file.attached_files['thumbnail'].mime_type).to eq('image/jpeg')
219
- end
220
-
221
- context "when the timeout is set" do
222
- before do
223
- Hydra::Derivatives::Processors::Video::Processor.timeout = 0.2 # 200ms
224
- end
225
- after do
226
- Hydra::Derivatives::Processors::Video::Processor.timeout = nil # clear timeout
227
- end
228
-
229
- it "raises a timeout" do
230
- expect { file.create_derivatives(filename) }.to raise_error Hydra::Derivatives::TimeoutError
231
- end
232
- end
233
- end
234
-
235
- describe "with an attached Powerpoint", requires_libreoffice: true do
236
- let(:filename) { File.expand_path('../../fixtures/FlashPix.ppt', __FILE__) }
237
- let(:attachment) { File.open(filename) }
238
- let(:file) do
239
- GenericFile.create(mime_type_from_fits: 'application/vnd.ms-powerpoint') do |t|
240
- t.original_file.content = attachment
241
- t.original_file.mime_type = t.mime_type_from_fits
242
- t.save
243
- end
244
- end
245
-
246
- it "transcodes" do
247
- file.create_derivatives(filename)
248
- file.reload
249
- expect(file.attached_files['original_file_thumbnail']).to have_content
250
- expect(file.attached_files['original_file_thumbnail'].mime_type).to eq('image/jpeg')
251
- expect(file.attached_files['original_file_access']).to have_content
252
- expect(file.attached_files['original_file_access'].mime_type).to eq('application/pdf')
253
- expect(file.attached_files['original_file_preservation']).to have_content
254
- expect(file.attached_files['original_file_preservation'].mime_type).to eq('application/vnd.openxmlformats-officedocument.presentationml.presentation')
255
- end
256
- end
257
-
258
- describe "with an attached rich text format", requires_libreoffice: true do
259
- let(:filename) { File.expand_path('../../fixtures/sample.rtf', __FILE__) }
260
- let(:attachment) { File.open(filename) }
261
- let(:file) do
262
- GenericFile.new(mime_type_from_fits: 'text/rtf').tap do |t|
263
- t.original_file.content = attachment
264
- t.original_file.mime_type = t.mime_type_from_fits
265
- t.save
266
- end
267
- end
268
-
269
- it "transcodes" do
270
- file.create_derivatives(filename)
271
- file.reload
272
- expect(file.attached_files['original_file_thumbnail']).to have_content
273
- expect(file.attached_files['original_file_thumbnail'].mime_type).to eq('image/jpeg')
274
- expect(file.attached_files['original_file_access']).to have_content
275
- expect(file.attached_files['original_file_access'].mime_type).to eq('application/pdf')
276
- expect(file.attached_files['original_file_preservation']).to have_content
277
- expect(file.attached_files['original_file_preservation'].mime_type).to eq('application/vnd.oasis.opendocument.text')
278
- end
279
- end
280
-
281
- describe "with an attached word doc format", requires_libreoffice: true do
282
- let(:filename) { File.expand_path('../../fixtures/test.doc', __FILE__) }
283
- let(:attachment) { File.open(filename) }
284
-
285
- let(:file) do
286
- GenericFile.new(mime_type_from_fits: 'application/msword').tap do |t|
287
- t.original_file.content = attachment
288
- t.original_file.mime_type = t.mime_type_from_fits
289
- t.save
290
- end
291
- end
292
-
293
- it "transcodes" do
294
- file.create_derivatives(filename)
295
- file.reload
296
- expect(file.attached_files['original_file_thumbnail']).to have_content
297
- expect(file.attached_files['original_file_thumbnail'].mime_type).to eq('image/jpeg')
298
- expect(file.attached_files['original_file_access']).to have_content
299
- expect(file.attached_files['original_file_access'].mime_type).to eq('application/pdf')
300
- expect(file.attached_files['original_file_preservation']).to have_content
301
- expect(file.attached_files['original_file_preservation'].mime_type).to eq('application/vnd.openxmlformats-officedocument.wordprocessingml.document')
302
- end
303
- end
304
-
305
- describe "with an attached excel format", requires_libreoffice: true do
306
- let(:filename) { File.expand_path('../../fixtures/test.xls', __FILE__) }
307
- let(:attachment) { File.open(filename) }
308
-
309
- let(:file) do
310
- GenericFile.new(mime_type_from_fits: 'application/vnd.ms-excel').tap do |t|
311
- t.original_file.content = attachment
312
- t.original_file.mime_type = t.mime_type_from_fits
313
- t.save
314
- end
315
- end
316
-
317
- it "transcodes" do
318
- file.create_derivatives(filename)
319
- file.reload
320
- expect(file.attached_files['original_file_thumbnail']).to have_content
321
- expect(file.attached_files['original_file_thumbnail'].mime_type).to eq('image/jpeg')
322
- expect(file.attached_files['original_file_access']).to have_content
323
- expect(file.attached_files['original_file_access'].mime_type).to eq('application/pdf')
324
- expect(file.attached_files['original_file_preservation']).to have_content
325
- expect(file.attached_files['original_file_preservation'].mime_type).to eq('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
326
- end
327
- end
328
-
329
- describe "with an attached tiff", requires_kdu_compress: true do
330
- let(:filename) { File.expand_path('../../fixtures/test.tif', __FILE__) }
331
- let(:attachment) { File.open(filename) }
332
-
333
- let(:file) do
334
- GenericFile.new(mime_type_from_fits: 'image/tiff').tap do |t|
335
- t.original_file.content = attachment
336
- t.original_file.mime_type = t.mime_type_from_fits
337
- t.save
338
- end
339
- end
340
-
341
- it "transcodes" do
342
- file.create_derivatives(filename)
343
- file.reload
344
- expect(file.attached_files['original_file_diy']).to have_content
345
- expect(file.attached_files['original_file_diy'].mime_type).to eq('image/jp2')
346
- expect(file.attached_files['config_lookup']).to have_content
347
- expect(file.attached_files['config_lookup'].mime_type).to eq('image/jp2')
348
- expect(file.attached_files['resized']).to have_content
349
- expect(file.attached_files['resized'].mime_type).to eq('image/jp2')
350
- expect(file.attached_files['string_recipe']).to have_content
351
- expect(file.attached_files['string_recipe'].mime_type).to eq('image/jp2')
352
- end
353
- end
354
- end