ezid-client 1.6.0 → 1.9.0
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 +5 -5
- data/.github/workflows/ruby.yml +35 -0
- data/README.md +59 -13
- data/Rakefile +6 -6
- data/VERSION +1 -1
- data/ezid-client.gemspec +5 -3
- data/lib/ezid/{batch_enumerator.rb → batch.rb} +2 -2
- data/lib/ezid/batch_download.rb +4 -0
- data/lib/ezid/client.rb +11 -10
- data/lib/ezid/configuration.rb +11 -4
- data/lib/ezid/error.rb +2 -0
- data/lib/ezid/identifier.rb +57 -20
- data/lib/ezid/metadata.rb +11 -1
- data/lib/ezid/metadata_transforms/datacite.rb +72 -0
- data/lib/ezid/requests/request.rb +22 -9
- data/lib/ezid/responses/response.rb +13 -0
- data/spec/fixtures/datacite_xml/empty.xml +1 -0
- data/spec/fixtures/datacite_xml/populated.xml +1 -0
- data/spec/integration/batch_download_spec.rb +4 -4
- data/spec/integration/client_spec.rb +1 -1
- data/spec/integration/identifier_spec.rb +3 -2
- data/spec/spec_helper.rb +4 -2
- data/spec/unit/{batch_enumerator_spec.rb → batch_spec.rb} +2 -2
- data/spec/unit/client_spec.rb +35 -3
- data/spec/unit/identifier_spec.rb +54 -15
- data/spec/unit/metadata_spec.rb +2 -0
- data/spec/unit/metadata_transform_datacite_spec.rb +169 -0
- metadata +54 -20
- data/.travis.yml +0 -10
data/lib/ezid/metadata.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "hashie"
|
2
|
+
require_relative "metadata_transforms/datacite"
|
2
3
|
|
3
4
|
module Ezid
|
4
5
|
#
|
@@ -85,7 +86,12 @@ module Ezid
|
|
85
86
|
end
|
86
87
|
|
87
88
|
def replace(data)
|
88
|
-
|
89
|
+
hsh = coerce(data)
|
90
|
+
|
91
|
+
# Perform additional profile transforms
|
92
|
+
MetadataTransformDatacite.inverse(hsh) if hsh["_profile"] == "datacite"
|
93
|
+
|
94
|
+
super hsh
|
89
95
|
end
|
90
96
|
|
91
97
|
# Output metadata in EZID ANVL format
|
@@ -94,6 +100,10 @@ module Ezid
|
|
94
100
|
def to_anvl(include_readonly = true)
|
95
101
|
hsh = to_h
|
96
102
|
hsh.reject! { |k, v| READONLY.include?(k) } unless include_readonly
|
103
|
+
|
104
|
+
# Perform additional profile transforms
|
105
|
+
MetadataTransformDatacite.transform(hsh) if profile == "datacite"
|
106
|
+
|
97
107
|
lines = hsh.map do |name, value|
|
98
108
|
element = [escape(ESCAPE_NAMES_RE, name), escape(ESCAPE_VALUES_RE, value)]
|
99
109
|
element.join(ANVL_SEPARATOR)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
3
|
+
module Ezid
|
4
|
+
class MetadataTransformDatacite
|
5
|
+
|
6
|
+
# Transforms the provided metadata hash into the appropriate format for datacite. Removes all "datacite.*" keys
|
7
|
+
# and transforms these to the appropriate datacite xml. The resultant xml is then added to a single "datacite" key.
|
8
|
+
def self.transform(hsh)
|
9
|
+
# Render the datacite xml
|
10
|
+
resource_opts = {
|
11
|
+
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
12
|
+
"xmlns" => "http://datacite.org/schema/kernel-4",
|
13
|
+
"xsi:schemaLocation" => "http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"
|
14
|
+
}
|
15
|
+
xml_builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") { |builder|
|
16
|
+
builder.resource(resource_opts) {
|
17
|
+
builder.identifier(identifierType: hsh["datacite.identifiertype"] || "DOI") {
|
18
|
+
builder.text hsh["datacite.identifier"]
|
19
|
+
}
|
20
|
+
builder.creators {
|
21
|
+
builder.creator {
|
22
|
+
builder.creatorName hsh["datacite.creator"]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
builder.titles {
|
26
|
+
builder.title hsh["datacite.title"]
|
27
|
+
}
|
28
|
+
builder.publisher hsh["datacite.publisher"]
|
29
|
+
builder.publicationYear hsh["datacite.publicationyear"]
|
30
|
+
builder.resourceType(resourceTypeGeneral: hsh["datacite.resourcetypegeneral"]) {
|
31
|
+
builder.text hsh["datacite.resourcetype"]
|
32
|
+
}
|
33
|
+
builder.descriptions {
|
34
|
+
builder.description(descriptionType: "Abstract") {
|
35
|
+
builder.text hsh["datacite.description"]
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
# Using this save option to prevent NG from rendering new lines and tabs
|
41
|
+
# between nodes. This to help with a cleaner anvl conversion. Similarly,
|
42
|
+
# the sub should just remove the new line after the xml header that NG
|
43
|
+
# adds, ex:
|
44
|
+
# <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resource ...
|
45
|
+
xml = xml_builder
|
46
|
+
.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
47
|
+
.sub("\n", "")
|
48
|
+
|
49
|
+
|
50
|
+
# Transform the hash
|
51
|
+
hsh.reject! { |k, v| k =~ /^datacite\./ }
|
52
|
+
hsh["datacite"] = xml
|
53
|
+
end
|
54
|
+
|
55
|
+
# Transforms the provided datacite metadata hash into the format appropriate for the Metadata class.
|
56
|
+
# Extracts appropriate fields from the datacite xml and creates the corresponding "datacite.*" keys
|
57
|
+
def self.inverse(hsh)
|
58
|
+
xml = Nokogiri::XML(hsh["datacite"])
|
59
|
+
xmlns = "http://datacite.org/schema/kernel-4"
|
60
|
+
hsh["datacite.identifier"] = xml.at_xpath("/ns:resource/ns:identifier/text()", ns: xmlns).to_s
|
61
|
+
hsh["datacite.identifiertype"] = xml.at_xpath("/ns:resource/ns:identifier/attribute::identifierType", ns: xmlns).to_s
|
62
|
+
hsh["datacite.creator"] = xml.at_xpath("/ns:resource/ns:creators/ns:creator/ns:creatorName/text()", ns: xmlns).to_s
|
63
|
+
hsh["datacite.title"] = xml.at_xpath("/ns:resource/ns:titles/ns:title/text()", ns: xmlns).to_s
|
64
|
+
hsh["datacite.publisher"] = xml.at_xpath("/ns:resource/ns:publisher/text()", ns: xmlns).to_s
|
65
|
+
hsh["datacite.publicationyear"] = xml.at_xpath("/ns:resource/ns:publicationYear/text()", ns: xmlns).to_s
|
66
|
+
hsh["datacite.resourcetype"] = xml.at_xpath("/ns:resource/ns:resourceType/text()", ns: xmlns).to_s
|
67
|
+
hsh["datacite.resourcetypegeneral"] = xml.at_xpath("/ns:resource/ns:resourceType/attribute::resourceTypeGeneral", ns: xmlns).to_s
|
68
|
+
hsh["datacite.description"] = xml.at_xpath("/ns:resource/ns:descriptions/ns:description/text()", ns: xmlns).to_s
|
69
|
+
hsh.delete("datacite")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'delegate'
|
2
|
+
require 'uri'
|
3
|
+
require 'net/http'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'date'
|
5
6
|
|
6
|
-
require_relative
|
7
|
+
require_relative '../responses/response'
|
7
8
|
|
8
9
|
module Ezid
|
9
10
|
#
|
@@ -31,7 +32,7 @@ module Ezid
|
|
31
32
|
end
|
32
33
|
|
33
34
|
def short_name
|
34
|
-
name.split(
|
35
|
+
name.split('::').last.sub('Request', '')
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
@@ -42,13 +43,24 @@ module Ezid
|
|
42
43
|
def initialize(client, *args)
|
43
44
|
@client = client
|
44
45
|
super build_request
|
45
|
-
set_content_type(
|
46
|
+
set_content_type('text/plain', charset: 'UTF-8')
|
46
47
|
end
|
47
48
|
|
48
49
|
# Executes the request and returns the response
|
49
50
|
# @return [Ezid::Response] the response
|
50
51
|
def execute
|
51
|
-
|
52
|
+
retries = 0
|
53
|
+
begin
|
54
|
+
response_class.new(get_response_for_request)
|
55
|
+
rescue Net::HTTPServerException, UnexpectedResponseError => e
|
56
|
+
if retries < 2
|
57
|
+
sleep 15
|
58
|
+
retries += 1
|
59
|
+
retry
|
60
|
+
else
|
61
|
+
raise
|
62
|
+
end
|
63
|
+
end
|
52
64
|
end
|
53
65
|
|
54
66
|
# The request URI
|
@@ -91,6 +103,7 @@ module Ezid
|
|
91
103
|
|
92
104
|
def get_response_for_request
|
93
105
|
connection.start do |conn|
|
106
|
+
self['Accept'] = 'text/plain'
|
94
107
|
add_authentication if authentication_required?
|
95
108
|
add_metadata if has_metadata?
|
96
109
|
conn.request(__getobj__)
|
@@ -113,7 +126,7 @@ module Ezid
|
|
113
126
|
# Adds authentication data to the request
|
114
127
|
def add_authentication
|
115
128
|
if session.open?
|
116
|
-
self[
|
129
|
+
self['Cookie'] = session.cookie
|
117
130
|
else
|
118
131
|
basic_auth(user, password)
|
119
132
|
end
|
@@ -14,6 +14,19 @@ module Ezid
|
|
14
14
|
# Error response status
|
15
15
|
ERROR = "error".freeze
|
16
16
|
|
17
|
+
def initialize(http_response)
|
18
|
+
super
|
19
|
+
|
20
|
+
unless __getobj__.code =~ /2\d\d/
|
21
|
+
raise Error, "HTTP response error: %s %s" %
|
22
|
+
[ __getobj__.code, __getobj__.message ]
|
23
|
+
end
|
24
|
+
|
25
|
+
unless status_line =~ /^(#{SUCCESS}|#{ERROR}): /
|
26
|
+
raise UnexpectedResponseError, __getobj__.body
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
17
30
|
# The response status -- "success" or "error"
|
18
31
|
# @return [String] the status
|
19
32
|
def status
|
@@ -0,0 +1 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"><identifier identifierType="DOI"></identifier><creators><creator><creatorName/></creator></creators><titles><title/></titles><publisher/><publicationYear/><resourceType resourceTypeGeneral=""></resourceType><descriptions><description descriptionType="Abstract"></description></descriptions></resource>
|
@@ -0,0 +1 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd"><identifier identifierType="TestIdentifierType">TestIdentifier</identifier><creators><creator><creatorName>TestCreatorName</creatorName></creator></creators><titles><title>TestTitle</title></titles><publisher>TestPublisher</publisher><publicationYear>TestPublicationYear</publicationYear><resourceType resourceTypeGeneral="TestResourceTypeGeneral">TestResourceType</resourceType><descriptions><description descriptionType="Abstract">TestDescription</description></descriptions></resource>
|
@@ -1,18 +1,18 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
|
3
3
|
module Ezid
|
4
|
-
RSpec.describe BatchDownload do
|
4
|
+
RSpec.describe BatchDownload, ezid: true do
|
5
5
|
|
6
6
|
subject do
|
7
7
|
a_week_ago = (Time.now - (7*24*60*60)).to_i
|
8
8
|
described_class.new(:anvl, compression: "zip", permanence: "test", status: "public", createdAfter: a_week_ago)
|
9
9
|
end
|
10
10
|
|
11
|
-
its(:download_url) { is_expected.to match(/\Ahttp:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/) }
|
12
|
-
|
13
11
|
specify {
|
12
|
+
expect(subject.download_url).to match(/\Ahttps:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/)
|
13
|
+
expect(subject.url).to match(/\Ahttps:\/\/ezid\.cdlib\.org\/download\/\w+\.zip\z/)
|
14
14
|
Dir.mktmpdir do |tmpdir|
|
15
|
-
expect(subject.
|
15
|
+
expect(subject.file(path: tmpdir))
|
16
16
|
.to match(/\A#{tmpdir}\/\w+\.zip\z/)
|
17
17
|
end
|
18
18
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Ezid
|
2
|
-
RSpec.describe Identifier,
|
2
|
+
RSpec.describe Identifier, ezid: true do
|
3
3
|
|
4
4
|
before {
|
5
5
|
@identifier = described_class.mint(TEST_ARK_SHOULDER, target: "http://example.com")
|
@@ -38,7 +38,8 @@ module Ezid
|
|
38
38
|
end
|
39
39
|
describe "delete" do
|
40
40
|
subject { described_class.mint(TEST_ARK_SHOULDER, status: "reserved") }
|
41
|
-
|
41
|
+
# Getting 400 Bad Request response - DCS 3/22/21
|
42
|
+
xit "deletes the identifier" do
|
42
43
|
subject.delete
|
43
44
|
expect(subject).to be_deleted
|
44
45
|
expect { described_class.find(subject.id) }.to raise_error(IdentifierNotFoundError)
|
data/spec/spec_helper.rb
CHANGED
@@ -15,6 +15,8 @@
|
|
15
15
|
#
|
16
16
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
17
17
|
|
18
|
+
require "byebug"
|
19
|
+
|
18
20
|
require "rspec/its"
|
19
21
|
|
20
22
|
require "ezid/test_helper"
|
@@ -61,7 +63,7 @@ RSpec.configure do |config|
|
|
61
63
|
|
62
64
|
# This setting enables warnings. It's recommended, but in some cases may
|
63
65
|
# be too noisy due to issues in dependencies.
|
64
|
-
config.warnings =
|
66
|
+
config.warnings = false
|
65
67
|
|
66
68
|
# Many RSpec users commonly either run the entire suite or an individual
|
67
69
|
# file, and it's useful to allow more verbose output when running an
|
@@ -76,7 +78,7 @@ RSpec.configure do |config|
|
|
76
78
|
# Print the 10 slowest examples and example groups at the
|
77
79
|
# end of the spec run, to help surface which specs are running
|
78
80
|
# particularly slow.
|
79
|
-
config.profile_examples = 5
|
81
|
+
#config.profile_examples = 5
|
80
82
|
|
81
83
|
# Run specs in random order to surface order dependencies. If you find an
|
82
84
|
# order dependency and want to debug it, you can fix the order by providing
|
data/spec/unit/client_spec.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
module Ezid
|
2
2
|
RSpec.describe Client do
|
3
3
|
|
4
|
+
let(:http_response) { double }
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(http_response).to receive(:value) { nil }
|
8
|
+
allow(http_response).to receive(:code) { '200' }
|
9
|
+
end
|
10
|
+
|
4
11
|
describe "initialization without a block" do
|
12
|
+
let(:http_response) { double }
|
5
13
|
it "should not login" do
|
6
14
|
expect_any_instance_of(described_class).not_to receive(:login)
|
7
15
|
described_class.new
|
@@ -151,9 +159,33 @@ EOS
|
|
151
159
|
end
|
152
160
|
|
153
161
|
describe "error handling" do
|
154
|
-
let(:
|
155
|
-
|
156
|
-
|
162
|
+
let(:stub_response) { GetIdentifierMetadataResponse.new(http_response) }
|
163
|
+
before do
|
164
|
+
allow(GetIdentifierMetadataRequest).to receive(:execute).with(subject, "invalid") { stub_response }
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "HTTP error response" do
|
168
|
+
before do
|
169
|
+
allow(http_response).to receive(:code) { '500' }
|
170
|
+
allow(http_response).to receive(:message) { 'Internal Server Error' }
|
171
|
+
end
|
172
|
+
it "should raise an exception" do
|
173
|
+
expect { subject.get_identifier_metadata("invalid") }.to raise_error(Error)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "EZID API error response" do
|
178
|
+
let(:http_response) { double(body: "error: bad request - no such identifier") }
|
179
|
+
it "should raise an exception" do
|
180
|
+
expect { subject.get_identifier_metadata("invalid") }.to raise_error(Error)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe "Unexpected response" do
|
185
|
+
let(:http_response) { double(body: "<html>\n<head><title>Ouch!</title></head>\n<body>Help!</body>\n</html>") }
|
186
|
+
it "should raise an exception" do
|
187
|
+
expect { subject.get_identifier_metadata("invalid") }.to raise_error(UnexpectedResponseError)
|
188
|
+
end
|
157
189
|
end
|
158
190
|
end
|
159
191
|
end
|
@@ -1,6 +1,39 @@
|
|
1
1
|
module Ezid
|
2
2
|
RSpec.describe Identifier do
|
3
|
+
|
3
4
|
describe "class methods" do
|
5
|
+
|
6
|
+
describe ".load" do
|
7
|
+
subject { described_class.load("ark:/99999/fk4086hs23", metadata) }
|
8
|
+
describe "with ANVL metadata" do
|
9
|
+
let(:metadata) do
|
10
|
+
<<-EOS
|
11
|
+
_updated: 1488227717
|
12
|
+
_target: http://example.com
|
13
|
+
_profile: erc
|
14
|
+
_ownergroup: apitest
|
15
|
+
_owner: apitest
|
16
|
+
_export: yes
|
17
|
+
_created: 1488227717
|
18
|
+
_status: public
|
19
|
+
EOS
|
20
|
+
end
|
21
|
+
its(:remote_metadata) {
|
22
|
+
is_expected.to eq({"_updated"=>"1488227717",
|
23
|
+
"_target"=>"http://example.com",
|
24
|
+
"_profile"=>"erc",
|
25
|
+
"_ownergroup"=>"apitest",
|
26
|
+
"_owner"=>"apitest",
|
27
|
+
"_export"=>"yes",
|
28
|
+
"_created"=>"1488227717",
|
29
|
+
"_status"=>"public"})
|
30
|
+
}
|
31
|
+
end
|
32
|
+
describe "with nil" do
|
33
|
+
let(:metadata) { nil }
|
34
|
+
its(:remote_metadata) { is_expected.to be_empty }
|
35
|
+
end
|
36
|
+
end
|
4
37
|
describe ".create" do
|
5
38
|
describe "with id and metadata args" do
|
6
39
|
it "instantiates a new Identifier and saves it" do
|
@@ -63,6 +96,7 @@ module Ezid
|
|
63
96
|
end
|
64
97
|
|
65
98
|
describe "instance methods" do
|
99
|
+
|
66
100
|
describe "#initialize" do
|
67
101
|
before {
|
68
102
|
allow(described_class).to receive(:defaults) { defaults }
|
@@ -129,7 +163,7 @@ module Ezid
|
|
129
163
|
its(:client) { is_expected.to_not eq(client) }
|
130
164
|
end
|
131
165
|
end
|
132
|
-
end
|
166
|
+
end # initialize
|
133
167
|
|
134
168
|
describe "#update" do
|
135
169
|
let(:metadata) { {"status" => "unavailable"} }
|
@@ -172,12 +206,23 @@ module Ezid
|
|
172
206
|
end
|
173
207
|
|
174
208
|
describe "#load_metadata" do
|
209
|
+
subject { described_class.new("id") }
|
175
210
|
let(:metadata) { "_profile: erc" }
|
176
|
-
before { allow(subject).to receive(:id) { "id" } }
|
177
211
|
it "replaces the remote metadata with metadata from EZID" do
|
178
212
|
expect(subject.client).to receive(:get_identifier_metadata).with("id") { double(id: "id", metadata: metadata) }
|
179
|
-
expect(subject.remote_metadata).to receive(:replace).with(metadata)
|
180
213
|
subject.load_metadata
|
214
|
+
expect(subject.remote_metadata).to eq({"_profile"=>"erc"})
|
215
|
+
expect(subject).to be_persisted
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#load_metadata!" do
|
220
|
+
subject { described_class.new("id") }
|
221
|
+
let(:metadata) { "_profile: erc" }
|
222
|
+
it "replaces the remote metadata with the provided metadata" do
|
223
|
+
subject.load_metadata!(metadata)
|
224
|
+
expect(subject.remote_metadata).to eq({"_profile"=>"erc"})
|
225
|
+
expect(subject).to be_persisted
|
181
226
|
end
|
182
227
|
end
|
183
228
|
|
@@ -323,23 +368,11 @@ module Ezid
|
|
323
368
|
context "when the status is \"unavailable\"" do
|
324
369
|
let(:status) { "#{Status::UNAVAILABLE} | whatever" }
|
325
370
|
context "and no reason is given" do
|
326
|
-
it "logs a warning" do
|
327
|
-
pending "https://github.com/duke-libraries/ezid-client/issues/46"
|
328
|
-
allow_message_expectations_on_nil
|
329
|
-
expect(subject.logger).to receive(:warn)
|
330
|
-
subject.unavailable!
|
331
|
-
end
|
332
371
|
it "does not change the status" do
|
333
372
|
expect { subject.unavailable! }.not_to change(subject, :status)
|
334
373
|
end
|
335
374
|
end
|
336
375
|
context "and a reason is given" do
|
337
|
-
it "logs a warning" do
|
338
|
-
pending "https://github.com/duke-libraries/ezid-client/issues/46"
|
339
|
-
allow_message_expectations_on_nil
|
340
|
-
expect(subject.logger).to receive(:warn)
|
341
|
-
subject.unavailable!("because")
|
342
|
-
end
|
343
376
|
it "should change the status" do
|
344
377
|
expect { subject.unavailable!("because") }.to change(subject, :status).from(status).to("#{Status::UNAVAILABLE} | because")
|
345
378
|
end
|
@@ -382,5 +415,11 @@ module Ezid
|
|
382
415
|
end
|
383
416
|
end
|
384
417
|
end
|
418
|
+
|
419
|
+
describe "#metadata" do
|
420
|
+
it "is frozen" do
|
421
|
+
expect { subject.metadata["foo"] = "bar" }.to raise_error(RuntimeError)
|
422
|
+
end
|
423
|
+
end
|
385
424
|
end
|
386
425
|
end
|